diff --git a/src/utils/blog-utils.ts b/src/utils/blog-utils.ts new file mode 100644 index 0000000..526c7f4 --- /dev/null +++ b/src/utils/blog-utils.ts @@ -0,0 +1,309 @@ +/** + * Blog utility functions + * This file contains common functions used across blog components + */ + +import { type BlogPost, type Lang } from '@/types'; +import { defaultLang } from '@/i18n/ui'; + +/** + * Get blog posts based on language + * @param lang - Current language + * @param postsGlob - Object containing imported posts from glob + * @returns Processed blog posts array + */ +export async function getBlogPosts(lang: Lang, postsGlob: Record): Promise { + // Get posts based on language + let allPosts = []; + + if (lang === 'zh') { + allPosts = postsGlob.zh; + } else { + allPosts = postsGlob.en; + } + + // Process blog post data + const posts: BlogPost[] = allPosts.map((post: any) => { + const slug = post.url?.split('/').filter(Boolean).pop() || ''; + + // Default image if not specified in frontmatter + const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center"; + + return { + title: post.frontmatter.title, + description: post.frontmatter.description || '', + image: post.frontmatter.image || defaultImage, + slug: slug, + tags: post.frontmatter.tags || [], + tagId: post.frontmatter.tagId || [], + category: Array.isArray(post.frontmatter.category) ? post.frontmatter.category : post.frontmatter.category ? [post.frontmatter.category] : [], + categoryId: Array.isArray(post.frontmatter.categoryId) ? post.frontmatter.categoryId : post.frontmatter.categoryId ? [post.frontmatter.categoryId] : [], + date: post.frontmatter.date || post.frontmatter.pubDate || '', + readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5 min read', + url: post.url || '', + }; + }); + + return posts; +} + +/** + * Sort blog posts by date (newest first) + * @param posts - Array of blog posts + * @returns Sorted array of blog posts + */ +export function sortPostsByDate(posts: BlogPost[]): BlogPost[] { + return posts + .filter(post => post.date) // Filter out posts without dates + .sort((a, b) => { + const dateA = new Date(a.date).getTime(); + const dateB = new Date(b.date).getTime(); + return dateB - dateA; // Descending order, newest first + }); +} + +/** + * Filter posts by category + * @param posts - Array of blog posts + * @param category - Category to filter by + * @returns Filtered array of blog posts + */ +export function filterPostsByCategory(posts: BlogPost[], category: string): BlogPost[] { + if (!category) return posts; + + return posts.filter(post => { + // First check if post contains the current category ID + if (post.categoryId && Array.isArray(post.categoryId)) { + return post.categoryId.some(catId => + catId.toLowerCase() === category.toLowerCase() + ); + } + // If no categoryId, check category + else if (post.category && Array.isArray(post.category)) { + return post.category.some(cat => + cat.toLowerCase() === category.toLowerCase() + ); + } + return false; + }); +} + +/** + * Filter posts by tag + * @param posts - Array of blog posts + * @param tag - Tag to filter by + * @returns Filtered array of blog posts + */ +export function filterPostsByTag(posts: BlogPost[], tag: string): BlogPost[] { + if (!tag) return posts; + + return posts.filter(post => { + // First check if post contains the current tag ID + if (post.tagId && Array.isArray(post.tagId)) { + return post.tagId.some(tagId => + tagId.toLowerCase() === tag.toLowerCase() + ); + } + // If no tagId, check tags + else if (post.tags && Array.isArray(post.tags)) { + return post.tags.some(postTag => + postTag.toLowerCase() === tag.toLowerCase() + ); + } + return false; + }); +} + +/** + * Extract categories from posts + * @param posts - Array of blog posts + * @returns Map of category names to category IDs + */ +export function extractCategories(posts: any[]): Map { + const allCategories = new Set(); + const categoryMap = new Map(); // Map of category name to ID + + posts.forEach(post => { + // Process categories + if (post.frontmatter?.category) { + const categories = Array.isArray(post.frontmatter.category) + ? post.frontmatter.category + : [post.frontmatter.category]; + + const categoryIds = Array.isArray(post.frontmatter.categoryId) + ? post.frontmatter.categoryId + : post.frontmatter.categoryId ? [post.frontmatter.categoryId] : []; + + // If categoryId exists, use mapping between categoryId and category + if (categoryIds.length > 0 && categoryIds.length === categories.length) { + categories.forEach((cat: string, index: number) => { + if (cat && categoryIds[index]) { + allCategories.add(cat); + categoryMap.set(cat, categoryIds[index]); + } + }); + } else { + // If no categoryId or lengths don't match, use category name as ID + categories.forEach((cat: string) => { + if (cat) { + allCategories.add(cat); + categoryMap.set(cat, cat.toLowerCase()); + } + }); + } + } + }); + + return categoryMap; +} + +/** + * Extract tags from posts + * @param posts - Array of blog posts + * @returns Map of tag names to tag IDs + */ +export function extractTags(posts: any[]): Map { + const allTags = new Set(); + const tagMap = new Map(); // Map of tag name to ID + + posts.forEach(post => { + // Process tags + if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) { + const tags = post.frontmatter.tags; + const tagIds = Array.isArray(post.frontmatter.tagId) + ? post.frontmatter.tagId + : post.frontmatter.tagId ? [post.frontmatter.tagId] : []; + + // If tagId exists, use mapping between tagId and tag + if (tagIds.length > 0 && tagIds.length === tags.length) { + tags.forEach((tag: string, index: number) => { + if (tag && tagIds[index]) { + allTags.add(tag); + tagMap.set(tag, tagIds[index]); + } + }); + } else { + // If no tagId or lengths don't match, use tag name as ID + tags.forEach((tag: string) => { + if (tag) { + allTags.add(tag); + tagMap.set(tag, tag.toLowerCase()); + } + }); + } + } + }); + + return tagMap; +} + +/** + * Get base URL for blog based on language + * @param lang - Current language + * @returns Base URL string + */ +export function getBlogBaseUrl(lang: Lang): string { + return lang === defaultLang ? '/blog' : `/${lang}/blog`; +} + +/** + * Process raw blog post data from import.meta.glob + * @param allPosts - Raw posts data from import.meta.glob + * @param lang - Current language + * @returns Processed blog posts array + */ +export function processRawBlogPosts(allPosts: Record[] | any[], lang: Lang): BlogPost[] { + // Process blog post data + const posts: BlogPost[] = allPosts.map((post: any) => { + const slug = post.url?.split('/').filter(Boolean).pop() || ''; + + // Default image if not specified in frontmatter + const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center"; + + // Default read time text based on language + const defaultReadTime = lang === 'zh' ? '5 分钟阅读' : '5 min read'; + + return { + title: post.frontmatter.title, + description: post.frontmatter.description || '', + image: post.frontmatter.image || defaultImage, + slug: slug, + tags: post.frontmatter.tags || [], + tagId: post.frontmatter.tagId || [], + category: Array.isArray(post.frontmatter.category) ? post.frontmatter.category : post.frontmatter.category ? [post.frontmatter.category] : [], + categoryId: Array.isArray(post.frontmatter.categoryId) ? post.frontmatter.categoryId : post.frontmatter.categoryId ? [post.frontmatter.categoryId] : [], + date: post.frontmatter.date || post.frontmatter.pubDate || '', + readTime: post.frontmatter.readTime || post.frontmatter.readingTime || defaultReadTime, + url: post.url || '', + }; + }); + + return posts; +} + +/** + * Extract post metadata for categories and tags + * @param posts - Array of blog posts + * @returns Array of post metadata + */ +export function extractPostMetadata(posts: any[]) { + return posts.map((post: any) => ({ + category: post.frontmatter?.category || [], + categoryId: post.frontmatter?.categoryId || [], + tags: post.frontmatter?.tags || [], + tagId: post.frontmatter?.tagId || [] + })); +} + +/** + * Get all blog posts with processing for a taxonomy page (category or tag) + * @param lang - Current language + * @param options - Additional options for processing + * @returns Object containing processed posts and metadata + */ +export async function getTaxonomyPageData(lang: Lang, options?: { + category?: string; + tag?: string; +}) { + // Read all blog posts + // Note: import.meta.glob only accepts literal strings, not variables + let allPosts; + + // Use specific glob patterns based on language + if (lang === 'zh') { + allPosts = await import.meta.glob('/src/pages/zh/blog/posts/*.{md,mdx}', { eager: true }); + } else { + allPosts = await import.meta.glob('/src/pages/blog/posts/*.{md,mdx}', { eager: true }); + } + + // Process blog post data + const blogPosts = processRawBlogPosts(Object.values(allPosts), lang); + + // Apply filters if specified + let filteredPosts = blogPosts; + + if (options?.category) { + filteredPosts = filterPostsByCategory(filteredPosts, options.category); + } + + if (options?.tag) { + filteredPosts = filterPostsByTag(filteredPosts, options.tag); + } + + // Sort posts by date + const sortedBlogPosts = sortPostsByDate(filteredPosts); + + // Extract categories and tags from all posts for sidebar + const allPostsMetadata = extractPostMetadata(Object.values(allPosts)); + + // Get categories and tags for sidebar + const categories = extractCategories(allPostsMetadata); + const tags = extractTags(allPostsMetadata); + + return { + posts: sortedBlogPosts, + categories, + tags, + allPosts + }; +} \ No newline at end of file