refactor(blog): extract blog utilities into shared module
Move common blog functionality like post filtering, sorting and data extraction into a centralized utils module. Replace Astro.glob with import.meta.glob for better performance. Update all blog components and pages to use the new utilities.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { type BlogPost, type Lang } from '@/types';
|
import { type BlogPost, type Lang } from '@/types';
|
||||||
|
import { getBlogBaseUrl, filterPostsByCategory, filterPostsByTag, sortPostsByDate } from '@/utils/blog-utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
posts?: BlogPost[];
|
posts?: BlogPost[];
|
||||||
@@ -9,31 +10,30 @@ interface Props {
|
|||||||
tag?: string;
|
tag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取组件属性
|
// Get component props
|
||||||
const { lang, baseUrl, category, tag, posts: providedPosts } = Astro.props;
|
const { lang, baseUrl, category, tag, posts: providedPosts } = Astro.props;
|
||||||
|
|
||||||
// 如果没有提供 posts,则直接从文件系统读取
|
// If posts are not provided, read from file system
|
||||||
let posts: BlogPost[] = [];
|
let posts: BlogPost[] = [];
|
||||||
|
|
||||||
if (providedPosts) {
|
if (providedPosts) {
|
||||||
// 使用提供的文章列表
|
// Use provided posts
|
||||||
posts = providedPosts;
|
posts = providedPosts;
|
||||||
} else {
|
} else {
|
||||||
// 根据当前语言读取博客文章
|
// Read blog posts based on current language
|
||||||
let allPosts = [];
|
const postsGlob = {
|
||||||
if (lang === 'zh') {
|
en: await import.meta.glob('../../pages/blog/posts/*.md', { eager: true }),
|
||||||
// 读取中文博客文章
|
zh: await import.meta.glob('../../pages/zh/blog/posts/*.md', { eager: true })
|
||||||
allPosts = await Astro.glob('../zh/blog/posts/*.md');
|
};
|
||||||
} else {
|
|
||||||
// 读取英文博客文章
|
|
||||||
allPosts = await Astro.glob('../blog/posts/*.md');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理博客文章数据
|
// Get posts for current language
|
||||||
posts = allPosts.map((post) => {
|
const allPosts = lang === 'zh' ? Object.values(postsGlob.zh) : Object.values(postsGlob.en);
|
||||||
|
|
||||||
|
// Process blog post data
|
||||||
|
posts = allPosts.map((post: any) => {
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
|
|
||||||
// 获取文章的默认图片,如果frontmatter中没有指定
|
// Default image if not specified in frontmatter
|
||||||
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -50,52 +50,18 @@ if (providedPosts) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按日期排序
|
// Sort posts by date
|
||||||
posts = posts
|
posts = sortPostsByDate(posts);
|
||||||
.filter(post => post.date) // 过滤掉没有日期的文章
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date).getTime();
|
|
||||||
const dateB = new Date(b.date).getTime();
|
|
||||||
return dateB - dateA; // 降序排列,最新的文章在前
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果指定了分类,则过滤文章
|
// Filter posts by category if specified
|
||||||
if (category) {
|
if (category) {
|
||||||
posts = posts.filter(post => {
|
posts = filterPostsByCategory(posts, category);
|
||||||
// 优先检查文章是否包含当前分类ID
|
|
||||||
if (post.categoryId && Array.isArray(post.categoryId)) {
|
|
||||||
return post.categoryId.some(catId =>
|
|
||||||
catId.toLowerCase() === category.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果没有 categoryId,则检查 category
|
|
||||||
else if (post.category && Array.isArray(post.category)) {
|
|
||||||
return post.category.some(cat =>
|
|
||||||
cat.toLowerCase() === category.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果指定了标签,则过滤文章
|
// Filter posts by tag if specified
|
||||||
if (tag) {
|
if (tag) {
|
||||||
posts = posts.filter(post => {
|
posts = filterPostsByTag(posts, tag);
|
||||||
// 优先检查文章是否包含当前标签ID
|
|
||||||
if (post.tagId && Array.isArray(post.tagId)) {
|
|
||||||
return post.tagId.some(tagId =>
|
|
||||||
tagId.toLowerCase() === tag.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果没有 tagId,则检查 tags
|
|
||||||
else if (post.tags && Array.isArray(post.tags)) {
|
|
||||||
return post.tags.some(postTag =>
|
|
||||||
postTag.toLowerCase() === tag.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调整基础URL
|
// 调整基础URL
|
||||||
|
|||||||
@@ -1,76 +1,42 @@
|
|||||||
---
|
---
|
||||||
// 导入必要的依赖
|
|
||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { getBlogBaseUrl, extractCategories } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 定义组件属性
|
// Define component props
|
||||||
interface Props {
|
interface Props {
|
||||||
lang?: Lang;
|
lang?: Lang;
|
||||||
currentCategory?: string;
|
currentCategory?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取组件属性
|
// Get component props
|
||||||
const { lang = defaultLang, currentCategory = '' } = Astro.props;
|
const { lang = defaultLang, currentCategory = '' } = Astro.props;
|
||||||
|
|
||||||
// 确定基础URL路径
|
// Determine base URL path
|
||||||
const baseUrl = lang === defaultLang ? '/blog' : `/${lang}/blog`;
|
const baseUrl = getBlogBaseUrl(lang);
|
||||||
|
|
||||||
// 读取所有博客文章 - 根据语言选择不同的静态路径
|
// Read all blog posts - choose different static paths based on language
|
||||||
let allPosts = [];
|
const postsGlob = {
|
||||||
if (lang === 'zh') {
|
en: await import.meta.glob('/src/pages/blog/posts/*.md', { eager: true }),
|
||||||
allPosts = await Astro.glob('/src/pages/zh/blog/posts/*.md');
|
zh: await import.meta.glob('/src/pages/zh/blog/posts/*.md', { eager: true })
|
||||||
} else {
|
};
|
||||||
allPosts = await Astro.glob('/src/pages/blog/posts/*.md');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集所有分类
|
// Get posts for current language
|
||||||
const allCategories = new Set<string>();
|
const allPosts = lang === 'zh' ? Object.values(postsGlob.zh) : Object.values(postsGlob.en);
|
||||||
|
|
||||||
// 处理所有文章的分类
|
// Extract categories from posts
|
||||||
const categoryMap = new Map<string, string>(); // 存储 categoryId 到 category 名称的映射
|
const categoryMap = extractCategories(allPosts);
|
||||||
|
|
||||||
allPosts.forEach(post => {
|
// Convert to array and sort
|
||||||
// 处理分类
|
const categories = Array.from(categoryMap.keys()).sort();
|
||||||
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] : [];
|
|
||||||
|
|
||||||
// 如果有 categoryId,则使用 categoryId 和 category 的映射
|
|
||||||
if (categoryIds.length > 0 && categoryIds.length === categories.length) {
|
|
||||||
categories.forEach((cat, index) => {
|
|
||||||
if (cat && categoryIds[index]) {
|
|
||||||
allCategories.add(cat);
|
|
||||||
categoryMap.set(cat, categoryIds[index]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 如果没有 categoryId 或长度不匹配,则使用 category 名称作为 ID
|
|
||||||
categories.forEach(cat => {
|
|
||||||
if (cat) {
|
|
||||||
allCategories.add(cat);
|
|
||||||
categoryMap.set(cat, cat.toLowerCase());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Multilingual titles
|
||||||
// 转换为数组并排序
|
|
||||||
const categories = Array.from(allCategories).sort();
|
|
||||||
|
|
||||||
// 多语言标题
|
|
||||||
const titles = {
|
const titles = {
|
||||||
en: 'Categories',
|
en: 'Categories',
|
||||||
zh: '分类'
|
zh: '分类'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取当前语言的标题
|
// Get title for current language
|
||||||
const title = titles[lang] || titles[defaultLang];
|
const title = titles[lang] || titles[defaultLang];
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,42 @@
|
|||||||
---
|
---
|
||||||
// 导入必要的依赖
|
|
||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { getBlogBaseUrl, extractTags } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 定义组件属性
|
// Define component props
|
||||||
interface Props {
|
interface Props {
|
||||||
lang?: Lang;
|
lang?: Lang;
|
||||||
currentTag?: string;
|
currentTag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取组件属性
|
// Get component props
|
||||||
const { lang = defaultLang, currentTag = '' } = Astro.props;
|
const { lang = defaultLang, currentTag = '' } = Astro.props;
|
||||||
|
|
||||||
// 确定基础URL路径
|
// Determine base URL path
|
||||||
const baseUrl = lang === defaultLang ? '/blog' : `/${lang}/blog`;
|
const baseUrl = getBlogBaseUrl(lang);
|
||||||
|
|
||||||
// 读取所有博客文章 - 根据语言选择不同的静态路径
|
// Read all blog posts - choose different static paths based on language
|
||||||
let allPosts = [];
|
const postsGlob = {
|
||||||
if (lang === 'zh') {
|
en: await import.meta.glob('/src/pages/blog/posts/*.md', { eager: true }),
|
||||||
allPosts = await Astro.glob('/src/pages/zh/blog/posts/*.md');
|
zh: await import.meta.glob('/src/pages/zh/blog/posts/*.md', { eager: true })
|
||||||
} else {
|
};
|
||||||
allPosts = await Astro.glob('/src/pages/blog/posts/*.md');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收集所有标签
|
// Get posts for current language
|
||||||
const allTags = new Set<string>();
|
const allPosts = lang === 'zh' ? Object.values(postsGlob.zh) : Object.values(postsGlob.en);
|
||||||
const tagMap = new Map<string, string>(); // 存储 tag 到 tagId 的映射
|
|
||||||
|
|
||||||
// 处理所有文章的标签
|
// Extract tags from posts
|
||||||
allPosts.forEach(post => {
|
const tagMap = extractTags(allPosts);
|
||||||
// 处理标签
|
|
||||||
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] : [];
|
|
||||||
|
|
||||||
// 如果有 tagId,则使用 tagId 和 tag 的映射
|
|
||||||
if (tagIds.length > 0 && tagIds.length === tags.length) {
|
|
||||||
tags.forEach((tag, index) => {
|
|
||||||
if (tag && tagIds[index]) {
|
|
||||||
allTags.add(tag);
|
|
||||||
tagMap.set(tag, tagIds[index]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 如果没有 tagId 或长度不匹配,则使用 tag 名称作为 ID
|
|
||||||
tags.forEach(tag => {
|
|
||||||
if (tag) {
|
|
||||||
allTags.add(tag);
|
|
||||||
tagMap.set(tag, tag.toLowerCase());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 转换为数组,添加 # 前缀,并排序
|
// Convert to array, add # prefix, and sort
|
||||||
const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
|
const tags = Array.from(tagMap.keys()).map(tag => `# ${tag}`).sort();
|
||||||
|
|
||||||
// 多语言标题
|
// Multilingual titles
|
||||||
const titles = {
|
const titles = {
|
||||||
en: '# Tags',
|
en: '# Tags',
|
||||||
zh: '# 标签'
|
zh: '# 标签'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取当前语言的标题
|
// Get title for current language
|
||||||
const title = titles[lang] || titles[defaultLang];
|
const title = titles[lang] || titles[defaultLang];
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
---
|
---
|
||||||
import { type BlogPost as BaseBlogPost } from '@/types';
|
import { type BlogPost as BaseBlogPost } from '@/types';
|
||||||
|
import { type Lang } from '@/i18n/utils';
|
||||||
|
|
||||||
|
// Get current path and language information
|
||||||
const currentPath = Astro.url.pathname;
|
const currentPath = Astro.url.pathname;
|
||||||
const isZh = currentPath.includes('/zh/');
|
const isZh = currentPath.includes('/zh/');
|
||||||
const lang = isZh ? 'zh' : 'en';
|
const lang = isZh ? 'zh' : 'en';
|
||||||
const currentSlug = currentPath.split('/').filter(Boolean).pop();
|
const currentSlug = currentPath.split('/').filter(Boolean).pop();
|
||||||
|
|
||||||
const enPosts = await Astro.glob('../../pages/blog/posts/*.md');
|
// Read blog posts using import.meta.glob
|
||||||
const zhPosts = await Astro.glob('../../pages/zh/blog/posts/*.md');
|
const postsGlob = {
|
||||||
const allPosts = isZh ? zhPosts : enPosts;
|
en: await import.meta.glob('../../pages/blog/posts/*.md', { eager: true }),
|
||||||
|
zh: await import.meta.glob('../../pages/zh/blog/posts/*.md', { eager: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get posts for current language
|
||||||
|
const allPosts = isZh ? Object.values(postsGlob.zh) : Object.values(postsGlob.en);
|
||||||
|
|
||||||
|
// Define BlogPost interface for navigation
|
||||||
interface BlogPost {
|
interface BlogPost {
|
||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -26,7 +33,8 @@ interface BlogPost {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const posts: BlogPost[] = allPosts.map((post) => {
|
// Process blog posts for navigation
|
||||||
|
const posts: BlogPost[] = allPosts.map((post: any) => {
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
return {
|
return {
|
||||||
title: post.frontmatter.title,
|
title: post.frontmatter.title,
|
||||||
@@ -44,7 +52,7 @@ const posts: BlogPost[] = allPosts.map((post) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort posts by date
|
||||||
const sortedPosts = posts
|
const sortedPosts = posts
|
||||||
.filter(post => post.frontmatter.date || post.frontmatter.pubDate)
|
.filter(post => post.frontmatter.date || post.frontmatter.pubDate)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
|||||||
@@ -6,48 +6,49 @@ import TagCard from '../../../components/blog/TagCard.astro';
|
|||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
import { type BlogPost } from '@/types';
|
import { type BlogPost } from '@/types';
|
||||||
|
import { filterPostsByCategory, sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 为动态路由生成静态路径
|
// Generate static paths for dynamic routing
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 收集所有分类ID或分类
|
// Collect all category IDs or categories
|
||||||
const uniqueCategories = new Set<string>();
|
const uniqueCategories = new Set<string>();
|
||||||
|
|
||||||
allPosts.forEach(post => {
|
Object.values(allPosts).forEach((post: any) => {
|
||||||
// 优先使用 categoryId 作为路由标识符
|
// Prioritize categoryId as route identifier
|
||||||
if (post.frontmatter?.categoryId) {
|
if (post.frontmatter?.categoryId) {
|
||||||
const categoryIds = Array.isArray(post.frontmatter.categoryId)
|
const categoryIds = Array.isArray(post.frontmatter.categoryId)
|
||||||
? post.frontmatter.categoryId
|
? post.frontmatter.categoryId
|
||||||
: [post.frontmatter.categoryId];
|
: [post.frontmatter.categoryId];
|
||||||
|
|
||||||
categoryIds.forEach(categoryId => {
|
categoryIds.forEach((categoryId: string) => {
|
||||||
if (categoryId) uniqueCategories.add(categoryId.toLowerCase());
|
if (categoryId) uniqueCategories.add(categoryId.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 如果没有 categoryId,则使用 category 作为后备
|
// If no categoryId, use category as fallback
|
||||||
else if (post.frontmatter?.category) {
|
else if (post.frontmatter?.category) {
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
const categories = Array.isArray(post.frontmatter.category)
|
||||||
? post.frontmatter.category
|
? post.frontmatter.category
|
||||||
: [post.frontmatter.category];
|
: [post.frontmatter.category];
|
||||||
|
|
||||||
categories.forEach(category => {
|
categories.forEach((category: string) => {
|
||||||
if (category) uniqueCategories.add(category.toLowerCase());
|
if (category) uniqueCategories.add(category.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 为每个分类生成路径
|
// Generate paths for each category
|
||||||
return Array.from(uniqueCategories).map(category => ({
|
return Array.from(uniqueCategories).map(category => ({
|
||||||
params: { category: encodeURIComponent(category) },
|
params: { category: encodeURIComponent(category) },
|
||||||
props: { category }
|
props: { category }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前语言环境
|
// Get current language environment
|
||||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
// 获取当前分类(从URL参数)
|
// Get current category (from URL parameters)
|
||||||
export interface Props {
|
export interface Props {
|
||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
@@ -55,129 +56,94 @@ export interface Props {
|
|||||||
const { category } = Astro.params;
|
const { category } = Astro.params;
|
||||||
const decodedCategory = category ? decodeURIComponent(category) : '';
|
const decodedCategory = category ? decodeURIComponent(category) : '';
|
||||||
|
|
||||||
|
// Read all blog posts using import.meta.glob
|
||||||
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 使用Astro.glob读取所有博客文章
|
// Process blog post data
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
const blogPosts: BlogPost[] = Object.values(allPosts).map((post: any) => {
|
||||||
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
// 处理博客文章数据
|
|
||||||
const blogPosts: BlogPost[] = allPosts
|
|
||||||
.filter(post => {
|
|
||||||
// 优先检查文章是否包含当前分类ID
|
|
||||||
if (post.frontmatter?.categoryId) {
|
|
||||||
const postCategoryIds = Array.isArray(post.frontmatter.categoryId)
|
|
||||||
? post.frontmatter.categoryId
|
|
||||||
: [post.frontmatter.categoryId];
|
|
||||||
|
|
||||||
return postCategoryIds.some(catId =>
|
|
||||||
catId.toLowerCase() === decodedCategory.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果没有 categoryId,则检查 category
|
|
||||||
else if (post.frontmatter?.category) {
|
|
||||||
const postCategories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
return postCategories.some(cat =>
|
|
||||||
cat.toLowerCase() === decodedCategory.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((post) => {
|
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
|
||||||
|
|
||||||
// 获取文章的默认图片,如果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 || [],
|
|
||||||
date: post.frontmatter.date || post.frontmatter.pubDate || '',
|
|
||||||
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5 min read',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 按日期排序
|
|
||||||
const sortedBlogPosts = blogPosts
|
|
||||||
.filter(post => post.date) // 过滤掉没有日期的文章
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date).getTime();
|
|
||||||
const dateB = new Date(b.date).getTime();
|
|
||||||
return dateB - dateA; // 降序排列,最新的文章在前
|
|
||||||
});
|
|
||||||
|
|
||||||
// 从所有博客文章中提取分类和标签(用于侧边栏)
|
|
||||||
const allCategories = new Set<string>();
|
|
||||||
const allTags = new Set<string>();
|
|
||||||
|
|
||||||
// 收集所有文章的分类和标签
|
|
||||||
allPosts.forEach(post => {
|
|
||||||
// 处理分类
|
|
||||||
if (post.frontmatter?.category) {
|
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
categories.forEach(cat => {
|
|
||||||
if (cat) allCategories.add(cat);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理标签
|
// Default image if not specified in frontmatter
|
||||||
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
||||||
post.frontmatter.tags.forEach(tag => {
|
|
||||||
if (tag) allTags.add(tag);
|
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',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 转换为数组并排序
|
// Filter posts by category
|
||||||
const categories = Array.from(allCategories).sort();
|
const filteredPosts = filterPostsByCategory(blogPosts, decodedCategory);
|
||||||
const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
|
|
||||||
|
|
||||||
// 查找与当前分类ID匹配的分类名称
|
// Sort posts by date
|
||||||
let displayCategoryName = "";
|
const sortedBlogPosts = sortPostsByDate(filteredPosts);
|
||||||
|
|
||||||
// 从博客文章中查找匹配的分类名称
|
// Find category name matching the current category ID
|
||||||
for (const post of allPosts) {
|
let categoryName = decodedCategory;
|
||||||
if (post.frontmatter?.categoryId && Array.isArray(post.frontmatter.categoryId) &&
|
|
||||||
post.frontmatter?.category && Array.isArray(post.frontmatter.category)) {
|
// Try to find matching category name from all posts
|
||||||
// 查找分类ID和分类名称的索引匹配
|
Object.values(allPosts).forEach((post: any) => {
|
||||||
const categoryIndex = post.frontmatter.categoryId.findIndex(id =>
|
// Check categoryId
|
||||||
|
if (post.frontmatter?.categoryId) {
|
||||||
|
const categoryIds = Array.isArray(post.frontmatter.categoryId)
|
||||||
|
? post.frontmatter.categoryId
|
||||||
|
: [post.frontmatter.categoryId];
|
||||||
|
|
||||||
|
const matchingCategoryIdIndex = categoryIds.findIndex((id: string) =>
|
||||||
id.toLowerCase() === decodedCategory.toLowerCase()
|
id.toLowerCase() === decodedCategory.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (categoryIndex !== -1 && categoryIndex < post.frontmatter.category.length) {
|
if (matchingCategoryIdIndex !== -1 && post.frontmatter.category) {
|
||||||
displayCategoryName = post.frontmatter.category[categoryIndex];
|
// If matching categoryId is found and post has category attribute
|
||||||
break;
|
const categories = Array.isArray(post.frontmatter.category)
|
||||||
|
? post.frontmatter.category
|
||||||
|
: [post.frontmatter.category];
|
||||||
|
|
||||||
|
// Ensure categories array length matches categoryIds
|
||||||
|
if (categories.length > matchingCategoryIdIndex) {
|
||||||
|
categoryName = categories[matchingCategoryIdIndex];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
// 如果没有找到匹配的分类名称,则使用分类ID并格式化(首字母大写)
|
// Page title and description
|
||||||
if (!displayCategoryName) {
|
const title = `${categoryName} - Blog | Joy Zhao`;
|
||||||
displayCategoryName = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1);
|
const description = `Explore articles about ${categoryName}. Dive into my thoughts on ${categoryName} and related topics.`;
|
||||||
}
|
|
||||||
|
|
||||||
// 动态生成页面标题和描述
|
// Extract categories and tags from all posts for sidebar
|
||||||
const pageTitle = `${displayCategoryName} - Blog | Joy Zhao`;
|
const allPostsArray = Object.values(allPosts).map((post: any) => ({
|
||||||
const pageDescription = `Explore articles about ${displayCategoryName}. Dive into my thoughts on ${displayCategoryName} and related topics.`;
|
category: post.frontmatter.category || [],
|
||||||
|
categoryId: post.frontmatter.categoryId || [],
|
||||||
|
tags: post.frontmatter.tags || [],
|
||||||
|
tagId: post.frontmatter.tagId || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get categories and tags for sidebar
|
||||||
|
|
||||||
|
// Get categories and tags for sidebar
|
||||||
|
const categories = extractCategories(allPostsArray);
|
||||||
|
const tags = extractTags(allPostsArray);
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogLayout title={pageTitle} description={pageDescription}>
|
<BlogLayout title={title} description={description}>
|
||||||
<main class="min-h-screen">
|
<main class="min-h-screen">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="container mx-auto px-4 pt-24 pb-12">
|
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
||||||
Category: <span class="text-purple-500">{displayCategoryName}</span>
|
Category: <span class="text-purple-500">{categoryName}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||||
Explore articles about {displayCategoryName}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}.
|
Explore articles about {categoryName}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,23 +153,23 @@ const pageDescription = `Explore articles about ${displayCategoryName}. Dive int
|
|||||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="lg:col-span-1 space-y-8">
|
<div class="lg:col-span-1 space-y-8">
|
||||||
<!-- 分类卡片 -->
|
<!-- Categories card -->
|
||||||
<CategoryCard lang="en" currentCategory={decodedCategory} />
|
<CategoryCard lang={lang} currentCategory={decodedCategory} />
|
||||||
|
|
||||||
<!-- 标签卡片 -->
|
<!-- Tags card -->
|
||||||
<TagCard lang="en" />
|
<TagCard lang={lang} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blog Posts -->
|
<!-- Blog Posts -->
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
{sortedBlogPosts.length > 0 ? (
|
{sortedBlogPosts.length > 0 ? (
|
||||||
<BlogList posts={sortedBlogPosts} lang="en" category={decodedCategory} />
|
<BlogList posts={sortedBlogPosts} lang={lang} category={decodedCategory} />
|
||||||
) : (
|
) : (
|
||||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border text-center">
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border text-center">
|
||||||
<h2 class="text-2xl font-semibold mb-4">No articles found</h2>
|
<h2 class="text-2xl font-semibold mb-4">No articles found</h2>
|
||||||
<p class="text-muted-foreground mb-6">There are no articles in this category yet. Check back later or explore other categories.</p>
|
<p class="text-muted-foreground mb-6">There are no articles in this category yet. Check back later or explore other categories.</p>
|
||||||
<a href="/blog" class="inline-flex items-center px-4 py-2 rounded-md bg-purple-500 text-white hover:bg-purple-600 transition-colors">
|
<a href={`/${lang}/blog`} class="inline-flex items-center px-4 py-2 rounded-md bg-purple-500 text-white hover:bg-purple-600 transition-colors">
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ import TagCard from '../../components/blog/TagCard.astro';
|
|||||||
import { type BlogPost } from '@/types';
|
import { type BlogPost } from '@/types';
|
||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { sortPostsByDate } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 使用Astro.currentLocale获取当前语言环境
|
// Get current language environment using Astro.currentLocale
|
||||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
// 使用Astro.glob读取所有博客文章
|
// Read all blog posts using import.meta.glob
|
||||||
const allPosts = await Astro.glob('./posts/*.md');
|
const allPosts = await import.meta.glob('./posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 处理博客文章数据
|
// Process blog post data
|
||||||
const blogPosts: BlogPost[] = allPosts.map((post) => {
|
const blogPosts: BlogPost[] = Object.values(allPosts).map((post: any) => {
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
|
|
||||||
// 获取文章的默认图片,如果frontmatter中没有指定
|
// Default image if not specified in frontmatter
|
||||||
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -34,43 +35,8 @@ const blogPosts: BlogPost[] = allPosts.map((post) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按日期排序
|
// Sort posts by date
|
||||||
const sortedBlogPosts = blogPosts
|
const sortedBlogPosts = sortPostsByDate(blogPosts);
|
||||||
.filter(post => post.date) // 过滤掉没有日期的文章
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date).getTime();
|
|
||||||
const dateB = new Date(b.date).getTime();
|
|
||||||
return dateB - dateA; // 降序排列,最新的文章在前
|
|
||||||
});
|
|
||||||
|
|
||||||
// 从博客文章中提取分类和标签
|
|
||||||
const allCategories = new Set<string>();
|
|
||||||
const allTags = new Set<string>();
|
|
||||||
|
|
||||||
// 收集所有文章的分类和标签
|
|
||||||
allPosts.forEach(post => {
|
|
||||||
// 处理分类
|
|
||||||
if (post.frontmatter?.category) {
|
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
categories.forEach(category => {
|
|
||||||
if (category) allCategories.add(category);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理标签
|
|
||||||
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
|
||||||
post.frontmatter.tags.forEach(tag => {
|
|
||||||
if (tag) allTags.add(tag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 转换为数组并排序
|
|
||||||
const categories = Array.from(allCategories).sort();
|
|
||||||
const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -6,40 +6,41 @@ import TagCard from '../../../components/blog/TagCard.astro';
|
|||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
import { type BlogPost } from '@/types';
|
import { type BlogPost } from '@/types';
|
||||||
|
import { filterPostsByTag, sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 为动态路由生成静态路径
|
// Generate static paths for dynamic routing
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 收集所有标签ID或标签
|
// Collect all tags
|
||||||
const uniqueTags = new Set<string>();
|
const uniqueTags = new Set<string>();
|
||||||
|
|
||||||
allPosts.forEach(post => {
|
Object.values(allPosts).forEach((post: any) => {
|
||||||
// 优先使用 tagId 作为路由标识符
|
// Prioritize tagId as route identifier
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
||||||
post.frontmatter.tagId.forEach(tagId => {
|
post.frontmatter.tagId.forEach((tagId: string) => {
|
||||||
if (tagId) uniqueTags.add(tagId.toLowerCase());
|
if (tagId) uniqueTags.add(tagId.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 如果没有 tagId,则使用 tags 作为后备
|
// If no tagId, use tags as fallback
|
||||||
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
||||||
post.frontmatter.tags.forEach(tag => {
|
post.frontmatter.tags.forEach((tag: string) => {
|
||||||
if (tag) uniqueTags.add(tag.toLowerCase());
|
if (tag) uniqueTags.add(tag.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 为每个标签生成路径,对包含特殊字符的标签进行编码
|
// Generate paths for each tag
|
||||||
return Array.from(uniqueTags).map(tag => ({
|
return Array.from(uniqueTags).map(tag => ({
|
||||||
params: { tag: encodeURIComponent(tag) },
|
params: { tag: encodeURIComponent(tag) },
|
||||||
props: { tag }
|
props: { tag }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前语言环境
|
// Get current language environment
|
||||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
// 获取当前标签(从URL参数)
|
// Get current tag (from URL parameters)
|
||||||
export interface Props {
|
export interface Props {
|
||||||
tag: string;
|
tag: string;
|
||||||
}
|
}
|
||||||
@@ -47,151 +48,113 @@ export interface Props {
|
|||||||
const { tag } = Astro.params;
|
const { tag } = Astro.params;
|
||||||
const decodedTag = tag ? decodeURIComponent(tag) : '';
|
const decodedTag = tag ? decodeURIComponent(tag) : '';
|
||||||
|
|
||||||
|
// Read all blog posts using import.meta.glob
|
||||||
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 使用Astro.glob读取所有博客文章
|
// Process blog post data
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
const blogPosts: BlogPost[] = Object.values(allPosts).map((post: any) => {
|
||||||
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
// 处理博客文章数据
|
|
||||||
const blogPosts: BlogPost[] = allPosts
|
|
||||||
.filter(post => {
|
|
||||||
// 优先检查文章是否包含当前标签ID
|
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
|
||||||
return post.frontmatter.tagId.some(postTagId =>
|
|
||||||
postTagId.toLowerCase() === decodedTag.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果没有 tagId,则检查 tags
|
|
||||||
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
|
||||||
return post.frontmatter.tags.some(postTag =>
|
|
||||||
postTag.toLowerCase() === decodedTag.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((post) => {
|
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
|
||||||
|
|
||||||
// 获取文章的默认图片,如果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 || [],
|
|
||||||
date: post.frontmatter.date || post.frontmatter.pubDate || '',
|
|
||||||
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5 min read',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 按日期排序
|
|
||||||
const sortedBlogPosts = blogPosts
|
|
||||||
.filter(post => post.date) // 过滤掉没有日期的文章
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date).getTime();
|
|
||||||
const dateB = new Date(b.date).getTime();
|
|
||||||
return dateB - dateA; // 降序排列,最新的文章在前
|
|
||||||
});
|
|
||||||
|
|
||||||
// 从所有博客文章中提取分类和标签(用于侧边栏)
|
|
||||||
const allCategories = new Set<string>();
|
|
||||||
const allTags = new Set<string>();
|
|
||||||
|
|
||||||
// 收集所有文章的分类和标签
|
|
||||||
allPosts.forEach(post => {
|
|
||||||
// 处理分类
|
|
||||||
if (post.frontmatter?.category) {
|
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
categories.forEach(cat => {
|
|
||||||
if (cat) allCategories.add(cat);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理标签
|
// Default image if not specified in frontmatter
|
||||||
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
||||||
post.frontmatter.tags.forEach(postTag => {
|
|
||||||
if (postTag) allTags.add(postTag);
|
return {
|
||||||
});
|
title: post.frontmatter.title,
|
||||||
}
|
description: post.frontmatter.description || '',
|
||||||
// 同时收集标签ID(用于内部路由)
|
image: post.frontmatter.image || defaultImage,
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
slug: slug,
|
||||||
// 这里我们不添加到 allTags 中,因为 tagId 只用于路由,不用于显示
|
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',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 转换为数组并排序
|
// Filter posts by tag
|
||||||
const categories = Array.from(allCategories).sort();
|
const filteredPosts = filterPostsByTag(blogPosts, decodedTag);
|
||||||
const tags = Array.from(allTags).map(postTag => `# ${postTag}`).sort();
|
|
||||||
|
|
||||||
// 查找与当前标签ID匹配的标签名称
|
// Sort posts by date
|
||||||
let displayTagName = "";
|
const sortedBlogPosts = sortPostsByDate(filteredPosts);
|
||||||
|
|
||||||
// 从博客文章中查找匹配的标签名称
|
// Extract categories and tags from all posts for sidebar
|
||||||
for (const post of allPosts) {
|
const allPostsArray = Object.values(allPosts).map((post: any) => ({
|
||||||
|
category: post.frontmatter.category || [],
|
||||||
|
categoryId: post.frontmatter.categoryId || [],
|
||||||
|
tags: post.frontmatter.tags || [],
|
||||||
|
tagId: post.frontmatter.tagId || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get categories and tags for sidebar
|
||||||
|
const categories = extractCategories(allPostsArray);
|
||||||
|
const tags = extractTags(allPostsArray);
|
||||||
|
|
||||||
|
// Find tag name matching the current tag ID
|
||||||
|
let tagName = decodedTag;
|
||||||
|
|
||||||
|
// Try to find matching tag name from all posts
|
||||||
|
Object.values(allPosts).forEach((post: any) => {
|
||||||
|
// Check tagId
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId) &&
|
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId) &&
|
||||||
post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
||||||
// 查找标签ID和标签名称的索引匹配
|
// Find matching index between tagId and tag name
|
||||||
const tagIndex = post.frontmatter.tagId.findIndex(id =>
|
const tagIndex = post.frontmatter.tagId.findIndex((id: string) =>
|
||||||
id.toLowerCase() === decodedTag.toLowerCase()
|
id.toLowerCase() === decodedTag.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tagIndex !== -1 && tagIndex < post.frontmatter.tags.length) {
|
if (tagIndex !== -1 && tagIndex < post.frontmatter.tags.length) {
|
||||||
displayTagName = post.frontmatter.tags[tagIndex];
|
tagName = post.frontmatter.tags[tagIndex];
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no matching tag name is found, use tag ID and format it (capitalize first letter)
|
||||||
|
if (tagName === decodedTag) {
|
||||||
|
tagName = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有找到匹配的标签名称,则使用标签ID并格式化(首字母大写)
|
// Generate page title and description
|
||||||
if (!displayTagName) {
|
const title = `#${tagName} - Blog | Joy Zhao`;
|
||||||
displayTagName = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
|
const description = `Explore articles tagged with #${tagName}. Dive into my thoughts on ${tagName} and related topics.`;
|
||||||
}
|
|
||||||
|
|
||||||
// 动态生成页面标题和描述
|
|
||||||
const pageTitle = `# ${displayTagName} - Blog | Joy Zhao`;
|
|
||||||
const pageDescription = `Explore articles tagged with # ${displayTagName}. Dive into my thoughts on ${displayTagName} and related topics.`;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogLayout title={pageTitle} description={pageDescription}>
|
<BlogLayout title={title} description={description}>
|
||||||
<main class="min-h-screen">
|
<main class="min-h-screen">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="container mx-auto px-4 pt-24 pb-12">
|
<section class="container mx-auto px-4 py-12">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
||||||
Tag: <span class="text-purple-500"># {displayTagName}</span>
|
Tag: <span class="text-purple-500">#{tagName}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||||
Explore articles tagged with # {displayTagName}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}.
|
Explore articles tagged with #{tagName}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Content Section -->
|
||||||
<div class="container mx-auto px-4 pb-20">
|
<section class="container mx-auto px-4 pb-16">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="lg:col-span-1 space-y-8">
|
<div class="lg:col-span-1 space-y-8">
|
||||||
<!-- 分类卡片 -->
|
<!-- Categories card -->
|
||||||
<CategoryCard lang="en" />
|
<CategoryCard lang={lang} />
|
||||||
|
|
||||||
<!-- 标签卡片 -->
|
<!-- Tags card -->
|
||||||
<TagCard lang="en" currentTag={decodedTag} />
|
<TagCard lang={lang} currentTag={decodedTag} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blog Posts -->
|
<!-- Main content -->
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
{sortedBlogPosts.length > 0 ? (
|
{sortedBlogPosts.length > 0 ? (
|
||||||
<BlogList posts={sortedBlogPosts} lang="en" tag={decodedTag} />
|
<BlogList posts={sortedBlogPosts} lang={lang} tag={decodedTag} />
|
||||||
) : (
|
) : (
|
||||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border text-center">
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border text-center">
|
||||||
<h2 class="text-2xl font-semibold mb-4">No articles found</h2>
|
<h2 class="text-2xl font-semibold mb-4">No articles found</h2>
|
||||||
<p class="text-muted-foreground mb-6">There are no articles with this tag yet. Check back later or explore other tags.</p>
|
<p class="text-muted-foreground mb-6">There are no articles with this tag yet. Check back later or explore other tags.</p>
|
||||||
<a href="/blog" class="inline-flex items-center px-4 py-2 rounded-md bg-purple-500 text-white hover:bg-purple-600 transition-colors">
|
<a href={`/${lang}/blog`} class="inline-flex items-center px-4 py-2 rounded-md bg-purple-500 text-white hover:bg-purple-600 transition-colors">
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -201,7 +164,7 @@ const pageDescription = `Explore articles tagged with # ${displayTagName}. Dive
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</BlogLayout>
|
</BlogLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -6,48 +6,49 @@ import TagCard from '../../../../components/blog/TagCard.astro';
|
|||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
import { type BlogPost } from '@/types';
|
import { type BlogPost } from '@/types';
|
||||||
|
import { filterPostsByCategory, sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 为动态路由生成静态路径
|
// Generate static paths for dynamic routing
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 收集所有分类ID或分类
|
// Collect all category IDs or categories
|
||||||
const uniqueCategories = new Set<string>();
|
const uniqueCategories = new Set<string>();
|
||||||
|
|
||||||
allPosts.forEach(post => {
|
Object.values(allPosts).forEach((post: any) => {
|
||||||
// 优先使用 categoryId 作为路由标识符
|
// Prioritize categoryId as route identifier
|
||||||
if (post.frontmatter?.categoryId) {
|
if (post.frontmatter?.categoryId) {
|
||||||
const categoryIds = Array.isArray(post.frontmatter.categoryId)
|
const categoryIds = Array.isArray(post.frontmatter.categoryId)
|
||||||
? post.frontmatter.categoryId
|
? post.frontmatter.categoryId
|
||||||
: [post.frontmatter.categoryId];
|
: [post.frontmatter.categoryId];
|
||||||
|
|
||||||
categoryIds.forEach(categoryId => {
|
categoryIds.forEach((categoryId: string) => {
|
||||||
if (categoryId) uniqueCategories.add(categoryId.toLowerCase());
|
if (categoryId) uniqueCategories.add(categoryId.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 如果没有 categoryId,则使用 category 作为后备
|
// If no categoryId, use category as fallback
|
||||||
else if (post.frontmatter?.category) {
|
else if (post.frontmatter?.category) {
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
const categories = Array.isArray(post.frontmatter.category)
|
||||||
? post.frontmatter.category
|
? post.frontmatter.category
|
||||||
: [post.frontmatter.category];
|
: [post.frontmatter.category];
|
||||||
|
|
||||||
categories.forEach(category => {
|
categories.forEach((category: string) => {
|
||||||
if (category) uniqueCategories.add(category.toLowerCase());
|
if (category) uniqueCategories.add(category.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 为每个分类生成路径
|
// Generate paths for each category
|
||||||
return Array.from(uniqueCategories).map(category => ({
|
return Array.from(uniqueCategories).map(category => ({
|
||||||
params: { category: encodeURIComponent(category) },
|
params: { category: encodeURIComponent(category) },
|
||||||
props: { category }
|
props: { category }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前语言环境
|
// Get current language environment
|
||||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
// 获取当前分类(从URL参数)
|
// Get current category (from URL parameters)
|
||||||
export interface Props {
|
export interface Props {
|
||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
@@ -55,155 +56,113 @@ export interface Props {
|
|||||||
const { category } = Astro.params;
|
const { category } = Astro.params;
|
||||||
const decodedCategory = category ? decodeURIComponent(category) : '';
|
const decodedCategory = category ? decodeURIComponent(category) : '';
|
||||||
|
|
||||||
|
// Read all blog posts using import.meta.glob
|
||||||
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 使用Astro.glob读取所有中文博客文章
|
// Process blog post data
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
const blogPosts: BlogPost[] = Object.values(allPosts).map((post: any) => {
|
||||||
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
// 处理博客文章数据
|
|
||||||
const blogPosts: BlogPost[] = allPosts
|
|
||||||
.filter(post => {
|
|
||||||
// 优先检查文章是否包含当前分类ID
|
|
||||||
if (post.frontmatter?.categoryId) {
|
|
||||||
const postCategoryIds = Array.isArray(post.frontmatter.categoryId)
|
|
||||||
? post.frontmatter.categoryId
|
|
||||||
: [post.frontmatter.categoryId];
|
|
||||||
|
|
||||||
return postCategoryIds.some(catId =>
|
|
||||||
catId.toLowerCase() === decodedCategory.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果没有 categoryId,则检查 category
|
|
||||||
else if (post.frontmatter?.category) {
|
|
||||||
const postCategories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
return postCategories.some(cat =>
|
|
||||||
cat.toLowerCase() === decodedCategory.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((post) => {
|
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
|
||||||
|
|
||||||
// 获取文章的默认图片,如果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 || [],
|
|
||||||
date: post.frontmatter.date || post.frontmatter.pubDate || '',
|
|
||||||
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5分钟阅读',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 按日期排序
|
|
||||||
const sortedBlogPosts = blogPosts
|
|
||||||
.filter(post => post.date) // 过滤掉没有日期的文章
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date).getTime();
|
|
||||||
const dateB = new Date(b.date).getTime();
|
|
||||||
return dateB - dateA; // 降序排列,最新的文章在前
|
|
||||||
});
|
|
||||||
|
|
||||||
// 从所有博客文章中提取分类和标签(用于侧边栏)
|
|
||||||
const allCategories = new Set<string>();
|
|
||||||
const allTags = new Set<string>();
|
|
||||||
|
|
||||||
// 收集所有文章的分类和标签
|
|
||||||
allPosts.forEach(post => {
|
|
||||||
// 处理分类
|
|
||||||
if (post.frontmatter?.category) {
|
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
categories.forEach(cat => {
|
|
||||||
if (cat) allCategories.add(cat);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理标签
|
// Default image if not specified in frontmatter
|
||||||
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
||||||
post.frontmatter.tags.forEach(tag => {
|
|
||||||
if (tag) allTags.add(tag);
|
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',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 转换为数组并排序
|
// Filter posts by category
|
||||||
const categories = Array.from(allCategories).sort();
|
const filteredPosts = filterPostsByCategory(blogPosts, decodedCategory);
|
||||||
const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
|
|
||||||
|
|
||||||
// 查找与当前分类ID匹配的分类名称
|
// Sort posts by date
|
||||||
let displayCategoryName = "";
|
const sortedBlogPosts = sortPostsByDate(filteredPosts);
|
||||||
|
|
||||||
// 从博客文章中查找匹配的分类名称
|
// Extract categories and tags from all posts for sidebar
|
||||||
for (const post of allPosts) {
|
const allPostsArray = Object.values(allPosts).map((post: any) => ({
|
||||||
|
category: post.frontmatter.category || [],
|
||||||
|
categoryId: post.frontmatter.categoryId || [],
|
||||||
|
tags: post.frontmatter.tags || [],
|
||||||
|
tagId: post.frontmatter.tagId || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get categories and tags for sidebar
|
||||||
|
const categories = extractCategories(allPostsArray);
|
||||||
|
const tags = extractTags(allPostsArray);
|
||||||
|
|
||||||
|
// Find category name matching the current category ID
|
||||||
|
let categoryName = decodedCategory;
|
||||||
|
|
||||||
|
// Try to find matching category name from all posts
|
||||||
|
Object.values(allPosts).forEach((post: any) => {
|
||||||
|
// Check categoryId
|
||||||
if (post.frontmatter?.categoryId && Array.isArray(post.frontmatter.categoryId) &&
|
if (post.frontmatter?.categoryId && Array.isArray(post.frontmatter.categoryId) &&
|
||||||
post.frontmatter?.category && Array.isArray(post.frontmatter.category)) {
|
post.frontmatter?.category && Array.isArray(post.frontmatter.category)) {
|
||||||
// 查找分类ID和分类名称的索引匹配
|
// Find matching index between categoryId and category name
|
||||||
const categoryIndex = post.frontmatter.categoryId.findIndex(id =>
|
const categoryIndex = post.frontmatter.categoryId.findIndex((id: string) =>
|
||||||
id.toLowerCase() === decodedCategory.toLowerCase()
|
id.toLowerCase() === decodedCategory.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (categoryIndex !== -1 && categoryIndex < post.frontmatter.category.length) {
|
if (categoryIndex !== -1 && categoryIndex < post.frontmatter.category.length) {
|
||||||
displayCategoryName = post.frontmatter.category[categoryIndex];
|
categoryName = post.frontmatter.category[categoryIndex];
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no matching category name is found, use category ID and format it (capitalize first letter)
|
||||||
|
if (categoryName === decodedCategory) {
|
||||||
|
categoryName = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有找到匹配的分类名称,则使用分类ID并格式化(首字母大写)
|
// Generate page title and description
|
||||||
if (!displayCategoryName) {
|
const title = `${categoryName} - 博客 | Joy Zhao`;
|
||||||
displayCategoryName = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1);
|
const description = `探索关于${categoryName}的文章。深入了解我对${categoryName}及相关主题的思考。`;
|
||||||
}
|
|
||||||
|
|
||||||
// 动态生成页面标题和描述
|
|
||||||
const pageTitle = `${displayCategoryName} - 博客 | 赵桂阳`;
|
|
||||||
const pageDescription = `探索关于${displayCategoryName}的文章。深入了解我对${displayCategoryName}和相关主题的思考。`;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogLayout title={pageTitle} description={pageDescription}>
|
<BlogLayout title={title} description={description}>
|
||||||
<main class="min-h-screen">
|
<main class="min-h-screen">
|
||||||
<!-- 头部区域 -->
|
<!-- Header Section -->
|
||||||
<div class="container mx-auto px-4 pt-24 pb-12">
|
<section class="container mx-auto px-4 py-12">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
||||||
分类: <span class="text-purple-500">{displayCategoryName}</span>
|
分类: <span class="text-purple-500">{categoryName}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||||
探索关于{displayCategoryName}的文章。找到 {sortedBlogPosts.length} 篇文章。
|
探索关于{categoryName}的文章。找到 {sortedBlogPosts.length} 篇文章。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- 主要内容 -->
|
<!-- Content Section -->
|
||||||
<div class="container mx-auto px-4 pb-20">
|
<section class="container mx-auto px-4 pb-16">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
<!-- 侧边栏 -->
|
<!-- Sidebar -->
|
||||||
<div class="lg:col-span-1 space-y-8">
|
<div class="lg:col-span-1 space-y-8">
|
||||||
<!-- 分类卡片 -->
|
<!-- Categories card -->
|
||||||
<CategoryCard lang="zh" currentCategory={decodedCategory} />
|
<CategoryCard lang={lang} currentCategory={decodedCategory} />
|
||||||
|
|
||||||
<!-- 标签卡片 -->
|
<!-- Tags card -->
|
||||||
<TagCard lang="zh" />
|
<TagCard lang={lang} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 博客文章 -->
|
<!-- Main content -->
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
{sortedBlogPosts.length > 0 ? (
|
{sortedBlogPosts.length > 0 ? (
|
||||||
<BlogList posts={sortedBlogPosts} lang="zh" category={decodedCategory} />
|
<BlogList posts={sortedBlogPosts} lang={lang} category={decodedCategory} />
|
||||||
) : (
|
) : (
|
||||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border text-center">
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-8 border border-border text-center">
|
||||||
<h2 class="text-2xl font-semibold mb-4">未找到文章</h2>
|
<h2 class="text-2xl font-semibold mb-4">没有找到文章</h2>
|
||||||
<p class="text-muted-foreground mb-6">该分类下暂时没有文章。请稍后再来查看或浏览其他分类。</p>
|
<p class="text-muted-foreground mb-6">该分类下暂时没有文章。请稍后再来查看或浏览其他分类。</p>
|
||||||
<a href="/zh/blog" class="inline-flex items-center px-4 py-2 rounded-md bg-purple-500 text-white hover:bg-purple-600 transition-colors">
|
<a href={`/${lang}/blog`} class="inline-flex items-center px-4 py-2 rounded-md bg-purple-500 text-white hover:bg-purple-600 transition-colors">
|
||||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -213,7 +172,7 @@ const pageDescription = `探索关于${displayCategoryName}的文章。深入了
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</BlogLayout>
|
</BlogLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ import TagCard from '../../../components/blog/TagCard.astro';
|
|||||||
import { type BlogPost } from '@/types';
|
import { type BlogPost } from '@/types';
|
||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 使用Astro.currentLocale获取当前语言环境
|
// 使用Astro.currentLocale获取当前语言环境
|
||||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
// 使用Astro.glob读取所有中文博客文章
|
// 使用import.meta.glob读取所有中文博客文章
|
||||||
const allPosts = await Astro.glob('./posts/*.md');
|
const allPosts = await import.meta.glob('./posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 处理博客文章数据
|
// 处理博客文章数据
|
||||||
const blogPosts: BlogPost[] = allPosts.map((post) => {
|
const blogPosts: BlogPost[] = Object.values(allPosts).map((post: any) => {
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
|
|
||||||
// 获取文章的默认图片,如果frontmatter中没有指定
|
// 获取文章的默认图片,如果frontmatter中没有指定
|
||||||
@@ -34,43 +35,20 @@ const blogPosts: BlogPost[] = allPosts.map((post) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按日期排序
|
// 使用工具函数按日期排序
|
||||||
const sortedBlogPosts = blogPosts
|
const sortedBlogPosts = sortPostsByDate(blogPosts);
|
||||||
.filter(post => post.date) // 过滤掉没有日期的文章
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date).getTime();
|
|
||||||
const dateB = new Date(b.date).getTime();
|
|
||||||
return dateB - dateA; // 降序排列,最新的文章在前
|
|
||||||
});
|
|
||||||
|
|
||||||
// 从博客文章中提取分类和标签
|
// 提取所有文章的分类和标签信息(用于侧边栏)
|
||||||
const allCategories = new Set<string>();
|
const allPostsArray = Object.values(allPosts).map((post: any) => ({
|
||||||
const allTags = new Set<string>();
|
category: post.frontmatter.category || [],
|
||||||
|
categoryId: post.frontmatter.categoryId || [],
|
||||||
|
tags: post.frontmatter.tags || [],
|
||||||
|
tagId: post.frontmatter.tagId || []
|
||||||
|
}));
|
||||||
|
|
||||||
// 收集所有文章的分类和标签
|
// 使用工具函数提取分类和标签
|
||||||
allPosts.forEach(post => {
|
const categories = extractCategories(allPostsArray);
|
||||||
// 处理分类
|
const tags = extractTags(allPostsArray);
|
||||||
if (post.frontmatter?.category) {
|
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
categories.forEach(category => {
|
|
||||||
if (category) allCategories.add(category);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理标签
|
|
||||||
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
|
||||||
post.frontmatter.tags.forEach(tag => {
|
|
||||||
if (tag) allTags.add(tag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 转换为数组并排序
|
|
||||||
const categories = Array.from(allCategories).sort();
|
|
||||||
const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -6,24 +6,25 @@ import TagCard from '../../../../components/blog/TagCard.astro';
|
|||||||
import { type Lang } from '@/i18n/utils';
|
import { type Lang } from '@/i18n/utils';
|
||||||
import { defaultLang } from '@/i18n/ui';
|
import { defaultLang } from '@/i18n/ui';
|
||||||
import { type BlogPost } from '@/types';
|
import { type BlogPost } from '@/types';
|
||||||
|
import { filterPostsByTag, sortPostsByDate, extractCategories, extractTags, getBlogBaseUrl } from '@/utils/blog-utils';
|
||||||
|
|
||||||
// 为动态路由生成静态路径
|
// 为动态路由生成静态路径
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
|
|
||||||
// 收集所有标签ID或标签
|
// 收集所有标签ID或标签
|
||||||
const uniqueTags = new Set<string>();
|
const uniqueTags = new Set<string>();
|
||||||
|
|
||||||
allPosts.forEach(post => {
|
Object.values(allPosts).forEach((post: any) => {
|
||||||
// 优先使用 tagId 作为路由标识符
|
// 优先使用 tagId 作为路由标识符
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
||||||
post.frontmatter.tagId.forEach(tagId => {
|
post.frontmatter.tagId.forEach((tagId: string) => {
|
||||||
if (tagId) uniqueTags.add(tagId.toLowerCase());
|
if (tagId) uniqueTags.add(tagId.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 如果没有 tagId,则使用 tags 作为后备
|
// 如果没有 tagId,则使用 tags 作为后备
|
||||||
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
||||||
post.frontmatter.tags.forEach(tag => {
|
post.frontmatter.tags.forEach((tag: string) => {
|
||||||
if (tag) uniqueTags.add(tag.toLowerCase());
|
if (tag) uniqueTags.add(tag.toLowerCase());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -47,143 +48,106 @@ export interface Props {
|
|||||||
const { tag } = Astro.params;
|
const { tag } = Astro.params;
|
||||||
const decodedTag = tag ? decodeURIComponent(tag) : '';
|
const decodedTag = tag ? decodeURIComponent(tag) : '';
|
||||||
|
|
||||||
|
// 使用import.meta.glob读取所有中文博客文章
|
||||||
// 使用Astro.glob读取所有中文博客文章
|
const allPosts = await import.meta.glob('../posts/*.md', { eager: true });
|
||||||
const allPosts = await Astro.glob('../posts/*.md');
|
|
||||||
|
|
||||||
// 处理博客文章数据
|
// 处理博客文章数据
|
||||||
const blogPosts: BlogPost[] = allPosts
|
const blogPosts: BlogPost[] = Object.values(allPosts).map((post: any) => {
|
||||||
.filter(post => {
|
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
||||||
// 优先检查文章是否包含当前标签ID
|
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
|
||||||
return post.frontmatter.tagId.some(postTagId =>
|
|
||||||
postTagId.toLowerCase() === decodedTag.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 如果没有 tagId,则检查 tags
|
|
||||||
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
|
||||||
return post.frontmatter.tags.some(postTag =>
|
|
||||||
postTag.toLowerCase() === decodedTag.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.map((post) => {
|
|
||||||
const slug = post.url?.split('/').filter(Boolean).pop() || '';
|
|
||||||
|
|
||||||
// 获取文章的默认图片,如果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 || [],
|
|
||||||
date: post.frontmatter.date || post.frontmatter.pubDate || '',
|
|
||||||
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5 分钟阅读',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 按日期排序
|
|
||||||
const sortedBlogPosts = blogPosts
|
|
||||||
.filter(post => post.date) // 过滤掉没有日期的文章
|
|
||||||
.sort((a, b) => {
|
|
||||||
const dateA = new Date(a.date).getTime();
|
|
||||||
const dateB = new Date(b.date).getTime();
|
|
||||||
return dateB - dateA; // 降序排列,最新的文章在前
|
|
||||||
});
|
|
||||||
|
|
||||||
// 从所有博客文章中提取分类和标签(用于侧边栏)
|
|
||||||
const allCategories = new Set<string>();
|
|
||||||
const allTags = new Set<string>();
|
|
||||||
|
|
||||||
// 收集所有文章的分类和标签
|
|
||||||
allPosts.forEach(post => {
|
|
||||||
// 处理分类
|
|
||||||
if (post.frontmatter?.category) {
|
|
||||||
const categories = Array.isArray(post.frontmatter.category)
|
|
||||||
? post.frontmatter.category
|
|
||||||
: [post.frontmatter.category];
|
|
||||||
|
|
||||||
categories.forEach(cat => {
|
|
||||||
if (cat) allCategories.add(cat);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理标签
|
// 获取文章的默认图片,如果frontmatter中没有指定
|
||||||
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
|
||||||
post.frontmatter.tags.forEach(postTag => {
|
|
||||||
if (postTag) allTags.add(postTag);
|
return {
|
||||||
});
|
title: post.frontmatter.title,
|
||||||
}
|
description: post.frontmatter.description || '',
|
||||||
// 同时收集标签ID(用于内部路由)
|
image: post.frontmatter.image || defaultImage,
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
|
slug: slug,
|
||||||
// 这里我们不添加到 allTags 中,因为 tagId 只用于路由,不用于显示
|
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 分钟阅读',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 转换为数组并排序
|
// 使用工具函数过滤与当前标签相关的文章
|
||||||
const categories = Array.from(allCategories).sort();
|
const filteredPosts = filterPostsByTag(blogPosts, decodedTag);
|
||||||
const tags = Array.from(allTags).map(postTag => `# ${postTag}`).sort();
|
|
||||||
|
// 使用工具函数按日期排序
|
||||||
|
const sortedBlogPosts = sortPostsByDate(filteredPosts);
|
||||||
|
|
||||||
|
// 提取所有文章的分类和标签信息(用于侧边栏)
|
||||||
|
const allPostsArray = Object.values(allPosts).map((post: any) => ({
|
||||||
|
category: post.frontmatter.category || [],
|
||||||
|
categoryId: post.frontmatter.categoryId || [],
|
||||||
|
tags: post.frontmatter.tags || [],
|
||||||
|
tagId: post.frontmatter.tagId || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 使用工具函数提取分类和标签
|
||||||
|
const categories = extractCategories(allPostsArray);
|
||||||
|
const tags = extractTags(allPostsArray);
|
||||||
|
|
||||||
// 查找与当前标签ID匹配的标签名称
|
// 查找与当前标签ID匹配的标签名称
|
||||||
let displayTagName = "";
|
let tagName = "";
|
||||||
|
|
||||||
// 从博客文章中查找匹配的标签名称
|
// 从博客文章中查找匹配的标签名称
|
||||||
for (const post of allPosts) {
|
for (const post of Object.values(allPosts)) {
|
||||||
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId) &&
|
const frontmatter = (post as any).frontmatter;
|
||||||
post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
if (frontmatter?.tagId && Array.isArray(frontmatter.tagId) &&
|
||||||
|
frontmatter?.tags && Array.isArray(frontmatter.tags)) {
|
||||||
// 查找标签ID和标签名称的索引匹配
|
// 查找标签ID和标签名称的索引匹配
|
||||||
const tagIndex = post.frontmatter.tagId.findIndex(id =>
|
const tagIndex = frontmatter.tagId.findIndex((id: string) =>
|
||||||
id.toLowerCase() === decodedTag.toLowerCase()
|
id.toLowerCase() === decodedTag.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tagIndex !== -1 && tagIndex < post.frontmatter.tags.length) {
|
if (tagIndex !== -1 && tagIndex < frontmatter.tags.length) {
|
||||||
displayTagName = post.frontmatter.tags[tagIndex];
|
tagName = frontmatter.tags[tagIndex];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有找到匹配的标签名称,则使用标签ID并格式化(首字母大写)
|
// 如果没有找到匹配的标签名称,则使用标签ID并格式化(首字母大写)
|
||||||
if (!displayTagName) {
|
if (!tagName) {
|
||||||
displayTagName = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
|
tagName = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态生成页面标题和描述
|
// 动态生成页面标题和描述
|
||||||
const pageTitle = `# ${displayTagName} - 博客 | Joy Zhao`;
|
const pageTitle = `# ${tagName} - 博客 | Joy Zhao`;
|
||||||
const pageDescription = `浏览带有 # ${displayTagName} 标签的文章。深入了解我关于 ${displayTagName} 及相关主题的想法。`;
|
const pageDescription = `浏览带有 # ${tagName} 标签的文章。深入了解我关于 ${tagName} 及相关主题的想法。`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogLayout title={pageTitle} description={pageDescription}>
|
<BlogLayout title={pageTitle} description={pageDescription}>
|
||||||
<main class="min-h-screen">
|
<main class="min-h-screen">
|
||||||
<!-- Header Section -->
|
<!-- 头部区域 -->
|
||||||
<div class="container mx-auto px-4 pt-24 pb-12">
|
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
||||||
标签: <span class="text-purple-500"># {displayTagName}</span>
|
标签: <span class="text-purple-500"># {tagName}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||||
浏览带有 # {displayTagName} 标签的文章。找到 {sortedBlogPosts.length} 篇文章。
|
浏览带有 # {tagName} 标签的文章。找到 {sortedBlogPosts.length} 篇文章。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- 主要内容 -->
|
||||||
<div class="container mx-auto px-4 pb-20">
|
<div class="container mx-auto px-4 pb-20">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<div class="lg:col-span-1 space-y-8">
|
<div class="lg:col-span-1 space-y-8">
|
||||||
<!-- 分类卡片 -->
|
<!-- 分类卡片 -->
|
||||||
<CategoryCard lang="zh" />
|
<CategoryCard lang="zh" currentCategory="" />
|
||||||
|
|
||||||
<!-- 标签卡片 -->
|
<!-- 标签卡片 -->
|
||||||
<TagCard lang="zh" currentTag={decodedTag} />
|
<TagCard lang="zh" currentTag={decodedTag} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Blog Posts -->
|
<!-- 博客文章列表 -->
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
{sortedBlogPosts.length > 0 ? (
|
{sortedBlogPosts.length > 0 ? (
|
||||||
<BlogList posts={sortedBlogPosts} lang="zh" tag={decodedTag} />
|
<BlogList posts={sortedBlogPosts} lang="zh" tag={decodedTag} />
|
||||||
|
|||||||
207
src/utils/blog-utils.ts
Normal file
207
src/utils/blog-utils.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
/**
|
||||||
|
* 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<string, any>): Promise<BlogPost[]> {
|
||||||
|
// 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<string, string> {
|
||||||
|
const allCategories = new Set<string>();
|
||||||
|
const categoryMap = new Map<string, string>(); // 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<string, string> {
|
||||||
|
const allTags = new Set<string>();
|
||||||
|
const tagMap = new Map<string, string>(); // 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`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user