import type { CollectionEntry } from 'astro:content' import { getCollection, render } from 'astro:content' import { defaultLocale } from '@/config' import { memoize } from '@/utils/cache' /** * Core Functions * - addMetaToPost * - getPosts * - checkPostSlugDuplication * * Post Filtering * - getRegularPosts * - getPinnedPosts * - getPostsByYear * * Tag Related * - getPostsGroupByTags * - getAllTags * - getPostsByTag * - getTagSupportedLangs */ // Type definitions export type Post = CollectionEntry<'posts'> & { remarkPluginFrontmatter: { minutes: number } } /** * Add metadata including reading time to a post * @param post The post to enhance with metadata * @returns Enhanced post with reading time information */ const metaCache = new Map() async function addMetaToPost(post: CollectionEntry<'posts'>): Promise { const cacheKey = `${post.id}-${post.data.lang || 'universal'}` if (!metaCache.has(cacheKey)) { const { remarkPluginFrontmatter } = await render(post) metaCache.set(cacheKey, remarkPluginFrontmatter as { minutes: number }) } return { ...post, remarkPluginFrontmatter: metaCache.get(cacheKey)!, } } /** * Find duplicate post slugs within the same language * @param posts Array of blog posts to check * @returns Array of descriptive error messages for duplicate slugs */ async function _checkPostSlugDuplication(posts: CollectionEntry<'posts'>[]): Promise { const slugMap = new Map>() const duplicates: string[] = [] posts.forEach((post) => { const lang = post.data.lang const slug = post.data.abbrlink || post.id if (!slugMap.has(lang)) { slugMap.set(lang, new Set()) } const slugSet = slugMap.get(lang)! if (slugSet.has(slug)) { if (!lang) { duplicates.push(`Duplicate slug "${slug}" found in universal post (applies to all languages)`) } else { duplicates.push(`Duplicate slug "${slug}" found in "${lang}" language post`) } } else { slugSet.add(slug) } }) return duplicates } // Export memoized version export const checkPostSlugDuplication = memoize(_checkPostSlugDuplication) /** * Get all posts (including pinned ones, excluding drafts in production) * @param lang The language code to filter by, defaults to site's default language * @returns Posts filtered by language, enhanced with metadata, sorted by date */ async function _getPosts(lang?: string) { const currentLang = lang || defaultLocale const filteredPosts = await getCollection( 'posts', ({ data }: CollectionEntry<'posts'>) => { // Show drafts in dev mode only const shouldInclude = import.meta.env.DEV || !data.draft return shouldInclude && (data.lang === currentLang || data.lang === '') }, ) const enhancedPosts = await Promise.all(filteredPosts.map(addMetaToPost)) return enhancedPosts.sort((a: Post, b: Post) => b.data.published.valueOf() - a.data.published.valueOf(), ) } // Export memoized version export const getPosts = memoize(_getPosts) /** * Get all non-pinned posts * @param lang The language code to filter by, defaults to site's default language * @returns Regular posts (non-pinned), filtered by language */ async function _getRegularPosts(lang?: string) { const posts = await getPosts(lang) return posts.filter(post => !post.data.pin) } // Export memoized version export const getRegularPosts = memoize(_getRegularPosts) /** * Get pinned posts sorted by pin priority * @param lang The language code to filter by, defaults to site's default language * @returns Pinned posts sorted by pin value in descending order */ async function _getPinnedPosts(lang?: string) { const posts = await getPosts(lang) return posts .filter(post => post.data.pin && post.data.pin > 0) .sort((a, b) => (b.data.pin || 0) - (a.data.pin || 0)) } // Export memoized version export const getPinnedPosts = memoize(_getPinnedPosts) /** * Group posts by year and sort within each year * @param lang The language code to filter by, defaults to site's default language * @returns Map of posts grouped by year (descending), sorted by date within each year */ async function _getPostsByYear(lang?: string): Promise> { const posts = await getRegularPosts(lang) const yearMap = new Map() posts.forEach((post: Post) => { const year = post.data.published.getFullYear() if (!yearMap.has(year)) { yearMap.set(year, []) } yearMap.get(year)!.push(post) }) yearMap.forEach((yearPosts) => { yearPosts.sort((a, b) => { const aDate = a.data.published const bDate = b.data.published return bDate.getMonth() - aDate.getMonth() || bDate.getDate() - aDate.getDate() }) }) return new Map([...yearMap.entries()].sort((a, b) => b[0] - a[0])) } // Export memoized version export const getPostsByYear = memoize(_getPostsByYear) /** * Group posts by their tags * @param lang The language code to filter by, defaults to site's default language * @returns Map where keys are tag names and values are arrays of posts with that tag */ async function _getPostsGroupByTags(lang?: string) { const posts = await getPosts(lang) const tagMap = new Map() posts.forEach((post: Post) => { post.data.tags?.forEach((tag: string) => { if (!tagMap.has(tag)) { tagMap.set(tag, []) } tagMap.get(tag)!.push(post) }) }) return tagMap } // Export memoized version export const getPostsGroupByTags = memoize(_getPostsGroupByTags) /** * Get all tags sorted by post count * @param lang The language code to filter by, defaults to site's default language * @returns Array of tags sorted by popularity (most posts first) */ async function _getAllTags(lang?: string) { const tagMap = await getPostsGroupByTags(lang) const tagsWithCount = Array.from(tagMap.entries()) tagsWithCount.sort((a, b) => b[1].length - a[1].length) return tagsWithCount.map(([tag]) => tag) } // Export memoized version export const getAllTags = memoize(_getAllTags) /** * Get all posts that contain a specific tag * @param tag The tag name to filter posts by * @param lang The language code to filter by, defaults to site's default language * @returns Array of posts that contain the specified tag */ async function _getPostsByTag(tag: string, lang?: string) { const tagMap = await getPostsGroupByTags(lang) return tagMap.get(tag) || [] } // Export memoized version export const getPostsByTag = memoize(_getPostsByTag) /** * Check which languages support a specific tag * @param tag The tag name to check language support for * @returns Array of language codes that support the specified tag */ async function _getTagSupportedLangs(tag: string) { const posts = await getCollection( 'posts', ({ data }) => !data.draft, ) const supportedLangs = [] const { allLocales } = await import('@/config') for (const locale of allLocales) { const hasPostsWithTag = posts.some(post => post.data.tags?.includes(tag) && (post.data.lang === locale || post.data.lang === ''), ) if (hasPostsWithTag) { supportedLangs.push(locale) } } return supportedLangs } // Export memoized version export const getTagSupportedLangs = memoize(_getTagSupportedLangs)