feat(blog): add tagId and categoryId for multilingual routing support

- Add tagId and categoryId fields to blog post frontmatter and interfaces
- Update blog list, category, and tag pages to use IDs for routing
- Implement fallback to regular tags/categories when IDs are not available
- Improve tag and category links with hover effects and proper encoding
- Update post meta component to support multilingual routing
This commit is contained in:
joyzhao
2025-06-19 14:08:47 +08:00
parent 601f3f06ce
commit deb80c0df7
13 changed files with 184 additions and 62 deletions

View File

@@ -48,14 +48,22 @@ export default function BlogList({ posts, lang, baseUrl = '/blog/posts/' }: Blog
{/* Tags */} {/* Tags */}
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{post.tags.map((tag, tagIndex) => ( {post.tags.map((tag, tagIndex) => {
<span // 使用 tagId 如果存在,否则使用 tag 本身
const tagRoute = post.tagId && Array.isArray(post.tagId) && tagIndex < post.tagId.length
? post.tagId[tagIndex]
: tag.toLowerCase();
return (
<a
href={`/${lang === 'en' ? '' : 'zh/'}blog/tags/${encodeURIComponent(tagRoute)}`}
key={`${tag}-${tagIndex}`} key={`${tag}-${tagIndex}`}
className="px-2 py-1 text-xs bg-muted text-muted-foreground rounded-full" className="px-2 py-1 text-xs bg-muted text-muted-foreground rounded-full hover:bg-muted/80 transition-colors"
> >
{tag} # {tag}
</span> </a>
))} );
})}
</div> </div>
</div> </div>

View File

@@ -7,7 +7,9 @@ export interface Props {
publishDate?: string; publishDate?: string;
readingTime?: number; readingTime?: number;
tags?: string[]; tags?: string[];
tagId?: string[];
category?: string | string[]; category?: string | string[];
categoryId?: string[] | string;
className?: string; className?: string;
} }
@@ -16,7 +18,9 @@ const {
publishDate, publishDate,
readingTime, readingTime,
tags, tags,
tagId,
category, category,
categoryId,
className = '' className = ''
} = Astro.props; } = Astro.props;
@@ -84,19 +88,26 @@ const getReadingTimeText = (minutes: number) => {
{category && ( {category && (
<div class="space-y-2"> <div class="space-y-2">
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div class="text-xs font-medium text-muted-foreground uppercase tracking-wide">
分类 {lang === 'zh' ? '分类' : 'Categories'}
</div> </div>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
{Array.isArray(category) ? ( {Array.isArray(category) ? (
category.map((cat) => ( category.map((cat, index) => {
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105"> // 获取对应的 categoryId如果存在
const catId = categoryId && Array.isArray(categoryId) && index < categoryId.length
? categoryId[index]
: cat.toLowerCase();
return (
<a href={`/${lang === 'en' ? '' : 'zh/'}blog/categories/${encodeURIComponent(catId)}`} class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105">
{cat} {cat}
</span> </a>
)) );
})
) : ( ) : (
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105"> <a href={`/${lang === 'en' ? '' : 'zh/'}blog/categories/${encodeURIComponent(categoryId?.toString() || category.toLowerCase())}`} class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105">
{category} {category}
</span> </a>
)} )}
</div> </div>
</div> </div>
@@ -106,14 +117,21 @@ const getReadingTimeText = (minutes: number) => {
{tags && tags.length > 0 && ( {tags && tags.length > 0 && (
<div class="space-y-2"> <div class="space-y-2">
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div class="text-xs font-medium text-muted-foreground uppercase tracking-wide">
标签 {lang === 'zh' ? '标签' : 'Tags'}
</div> </div>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
{tags.map((tag) => ( {tags.map((tag, index) => {
<span class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors border border-gray-200 dark:border-gray-600"> // 获取对应的 tagId如果存在
const tagRoute = tagId && Array.isArray(tagId) && index < tagId.length
? tagId[index]
: tag.toLowerCase();
return (
<a href={`/${lang === 'en' ? '' : 'zh/'}blog/tags/${encodeURIComponent(tagRoute)}`} class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors border border-gray-200 dark:border-gray-600">
# {tag} # {tag}
</span> </a>
))} );
})}
</div> </div>
</div> </div>
)} )}

View File

@@ -23,7 +23,9 @@ const {
publishDate, publishDate,
date, date,
tags, tags,
tagId,
category, category,
categoryId,
readTime, readTime,
} = frontmatter; } = frontmatter;
@@ -85,7 +87,9 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
publishDate={finalPublishDate} publishDate={finalPublishDate}
readingTime={finalReadingTime} readingTime={finalReadingTime}
tags={tags} tags={tags}
tagId={tagId}
category={category} category={category}
categoryId={categoryId}
className="justify-start" className="justify-start"
/> />
</header> </header>

View File

@@ -9,11 +9,22 @@ import { type BlogPost } from '@/types';
export async function getStaticPaths() { export async function getStaticPaths() {
const allPosts = await Astro.glob('../posts/*.md'); const allPosts = await Astro.glob('../posts/*.md');
// 收集所有分类 // 收集所有分类ID或分类
const uniqueCategories = new Set<string>(); const uniqueCategories = new Set<string>();
allPosts.forEach(post => { allPosts.forEach(post => {
if (post.frontmatter?.category) { // 优先使用 categoryId 作为路由标识符
if (post.frontmatter?.categoryId) {
const categoryIds = Array.isArray(post.frontmatter.categoryId)
? post.frontmatter.categoryId
: [post.frontmatter.categoryId];
categoryIds.forEach(categoryId => {
if (categoryId) uniqueCategories.add(categoryId.toLowerCase());
});
}
// 如果没有 categoryId则使用 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];
@@ -49,8 +60,18 @@ const allPosts = await Astro.glob('../posts/*.md');
// 处理博客文章数据 // 处理博客文章数据
const blogPosts: BlogPost[] = allPosts const blogPosts: BlogPost[] = allPosts
.filter(post => { .filter(post => {
// 检查文章是否属于当前分类 // 优先检查文章是否包含当前分类ID
if (post.frontmatter?.category) { 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) const postCategories = Array.isArray(post.frontmatter.category)
? post.frontmatter.category ? post.frontmatter.category
: [post.frontmatter.category]; : [post.frontmatter.category];
@@ -114,7 +135,7 @@ allPosts.forEach(post => {
// 转换为数组并排序 // 转换为数组并排序
const categories = Array.from(allCategories).sort(); const categories = Array.from(allCategories).sort();
const tags = Array.from(allTags).map(tag => `#${tag}`).sort(); const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
// 获取当前分类的格式化名称(首字母大写) // 获取当前分类的格式化名称(首字母大写)
const formattedCategory = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1); const formattedCategory = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1);
@@ -171,7 +192,7 @@ const pageDescription = `Explore articles about ${formattedCategory}. Dive into
</h3> </h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{tags.map((tag) => ( {tags.map((tag) => (
<a href={`/blog/tags/${encodeURIComponent(tag.slice(1).toLowerCase())}`} <a href={`/blog/tags/${encodeURIComponent(tag.slice(2).toLowerCase())}`}
class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200"> class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200">
{tag} {tag}
</a> </a>

View File

@@ -24,6 +24,9 @@ const blogPosts: BlogPost[] = allPosts.map((post) => {
image: post.frontmatter.image || defaultImage, image: post.frontmatter.image || defaultImage,
slug: slug, slug: slug,
tags: post.frontmatter.tags || [], 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 || '', date: post.frontmatter.date || post.frontmatter.pubDate || '',
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5 min read', readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5 min read',
}; };
@@ -65,7 +68,7 @@ allPosts.forEach(post => {
// 转换为数组并排序 // 转换为数组并排序
const categories = Array.from(allCategories).sort(); const categories = Array.from(allCategories).sort();
const tags = Array.from(allTags).map(tag => `#${tag}`).sort(); const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
--- ---
@@ -98,7 +101,7 @@ const tags = Array.from(allTags).map(tag => `#${tag}`).sort();
</h3> </h3>
<div class="space-y-2"> <div class="space-y-2">
{categories.map((category) => ( {categories.map((category) => (
<a href={`/blog/categories/${category.toLowerCase()}`} <a href={`/blog/categories/${encodeURIComponent(category.toLowerCase())}`}
class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200"> class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200">
{category} {category}
</a> </a>
@@ -116,7 +119,7 @@ const tags = Array.from(allTags).map(tag => `#${tag}`).sort();
</h3> </h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{tags.map((tag) => ( {tags.map((tag) => (
<a href={`/blog/tags/${tag.slice(1).toLowerCase()}`} <a href={`/blog/tags/${encodeURIComponent(tag.slice(2).toLowerCase())}`}
class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200"> class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200">
{tag} {tag}
</a> </a>

View File

@@ -5,7 +5,9 @@ image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400&h=250
date: "May 10, 2025" date: "May 10, 2025"
readTime: "5 min read" readTime: "5 min read"
tags: ["React", "JavaScript", "Frontend"] tags: ["React", "JavaScript", "Frontend"]
tagId: ["react", "javascript", "frontend"]
category: ["React", "Frontend"] category: ["React", "Frontend"]
categoryId: ["react", "frontend"]
slug: "mastering-react-hooks" slug: "mastering-react-hooks"
layout: "../../../layouts/BlogPostLayout.astro" layout: "../../../layouts/BlogPostLayout.astro"
--- ---

View File

@@ -9,11 +9,18 @@ import { type BlogPost } from '@/types';
export async function getStaticPaths() { export async function getStaticPaths() {
const allPosts = await Astro.glob('../posts/*.md'); const allPosts = await Astro.glob('../posts/*.md');
// 收集所有标签 // 收集所有标签ID或标签
const uniqueTags = new Set<string>(); const uniqueTags = new Set<string>();
allPosts.forEach(post => { allPosts.forEach(post => {
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) { // 优先使用 tagId 作为路由标识符
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
post.frontmatter.tagId.forEach(tagId => {
if (tagId) uniqueTags.add(tagId.toLowerCase());
});
}
// 如果没有 tagId则使用 tags 作为后备
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
post.frontmatter.tags.forEach(tag => { post.frontmatter.tags.forEach(tag => {
if (tag) uniqueTags.add(tag.toLowerCase()); if (tag) uniqueTags.add(tag.toLowerCase());
}); });
@@ -45,8 +52,14 @@ const allPosts = await Astro.glob('../posts/*.md');
// 处理博客文章数据 // 处理博客文章数据
const blogPosts: BlogPost[] = allPosts const blogPosts: BlogPost[] = allPosts
.filter(post => { .filter(post => {
// 检查文章是否包含当前标签 // 优先检查文章是否包含当前标签ID
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) { 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 => return post.frontmatter.tags.some(postTag =>
postTag.toLowerCase() === decodedTag.toLowerCase() postTag.toLowerCase() === decodedTag.toLowerCase()
); );
@@ -102,18 +115,22 @@ allPosts.forEach(post => {
if (postTag) allTags.add(postTag); if (postTag) allTags.add(postTag);
}); });
} }
// 同时收集标签ID用于内部路由
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
// 这里我们不添加到 allTags 中,因为 tagId 只用于路由,不用于显示
}
}); });
// 转换为数组并排序 // 转换为数组并排序
const categories = Array.from(allCategories).sort(); const categories = Array.from(allCategories).sort();
const tags = Array.from(allTags).map(postTag => `#${postTag}`).sort(); const tags = Array.from(allTags).map(postTag => `# ${postTag}`).sort();
// 获取当前标签的格式化名称(首字母大写) // 获取当前标签的格式化名称(首字母大写)
const formattedTag = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1); const formattedTag = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
// 动态生成页面标题和描述 // 动态生成页面标题和描述
const pageTitle = `#${formattedTag} - Blog | Joy Zhao`; const pageTitle = `# ${formattedTag} - Blog | Joy Zhao`;
const pageDescription = `Explore articles tagged with #${formattedTag}. Dive into my thoughts on ${formattedTag} and related topics.`; const pageDescription = `Explore articles tagged with # ${formattedTag}. Dive into my thoughts on ${formattedTag} and related topics.`;
--- ---
<BlogLayout title={pageTitle} description={pageDescription}> <BlogLayout title={pageTitle} description={pageDescription}>
@@ -122,10 +139,10 @@ const pageDescription = `Explore articles tagged with #${formattedTag}. Dive int
<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">
Tag: <span class="text-purple-500">#{formattedTag}</span> Tag: <span class="text-purple-500"># {formattedTag}</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 #{formattedTag}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}. Explore articles tagged with # {formattedTag}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}.
</p> </p>
</div> </div>
</div> </div>
@@ -163,7 +180,7 @@ const pageDescription = `Explore articles tagged with #${formattedTag}. Dive int
</h3> </h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{tags.map((tagItem) => { {tags.map((tagItem) => {
const tagName = tagItem.slice(1).toLowerCase(); const tagName = tagItem.slice(2).toLowerCase();
const isCurrentTag = tagName === decodedTag.toLowerCase(); const isCurrentTag = tagName === decodedTag.toLowerCase();
return ( return (
<a href={`/blog/tags/${encodeURIComponent(tagName)}`} <a href={`/blog/tags/${encodeURIComponent(tagName)}`}

View File

@@ -9,11 +9,22 @@ import { type BlogPost } from '@/types';
export async function getStaticPaths() { export async function getStaticPaths() {
const allPosts = await Astro.glob('../posts/*.md'); const allPosts = await Astro.glob('../posts/*.md');
// 收集所有分类 // 收集所有分类ID或分类
const uniqueCategories = new Set<string>(); const uniqueCategories = new Set<string>();
allPosts.forEach(post => { allPosts.forEach(post => {
if (post.frontmatter?.category) { // 优先使用 categoryId 作为路由标识符
if (post.frontmatter?.categoryId) {
const categoryIds = Array.isArray(post.frontmatter.categoryId)
? post.frontmatter.categoryId
: [post.frontmatter.categoryId];
categoryIds.forEach(categoryId => {
if (categoryId) uniqueCategories.add(categoryId.toLowerCase());
});
}
// 如果没有 categoryId则使用 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];
@@ -49,8 +60,18 @@ const allPosts = await Astro.glob('../posts/*.md');
// 处理博客文章数据 // 处理博客文章数据
const blogPosts: BlogPost[] = allPosts const blogPosts: BlogPost[] = allPosts
.filter(post => { .filter(post => {
// 检查文章是否属于当前分类 // 优先检查文章是否包含当前分类ID
if (post.frontmatter?.category) { 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) const postCategories = Array.isArray(post.frontmatter.category)
? post.frontmatter.category ? post.frontmatter.category
: [post.frontmatter.category]; : [post.frontmatter.category];
@@ -114,7 +135,7 @@ allPosts.forEach(post => {
// 转换为数组并排序 // 转换为数组并排序
const categories = Array.from(allCategories).sort(); const categories = Array.from(allCategories).sort();
const tags = Array.from(allTags).map(tag => `#${tag}`).sort(); const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
// 获取当前分类的格式化名称(首字母大写) // 获取当前分类的格式化名称(首字母大写)
const formattedCategory = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1); const formattedCategory = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1);
@@ -171,7 +192,7 @@ const pageDescription = `探索关于${formattedCategory}的文章。深入了
</h3> </h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{tags.map((tag) => ( {tags.map((tag) => (
<a href={`/zh/blog/tags/${encodeURIComponent(tag.slice(1).toLowerCase())}`} <a href={`/zh/blog/tags/${encodeURIComponent(tag.slice(2).toLowerCase())}`}
class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200"> class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200">
{tag} {tag}
</a> </a>

View File

@@ -24,6 +24,9 @@ const blogPosts: BlogPost[] = allPosts.map((post) => {
image: post.frontmatter.image || defaultImage, image: post.frontmatter.image || defaultImage,
slug: slug, slug: slug,
tags: post.frontmatter.tags || [], 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 || '', date: post.frontmatter.date || post.frontmatter.pubDate || '',
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5分钟阅读', readTime: post.frontmatter.readTime || post.frontmatter.readingTime || '5分钟阅读',
}; };
@@ -65,7 +68,7 @@ allPosts.forEach(post => {
// 转换为数组并排序 // 转换为数组并排序
const categories = Array.from(allCategories).sort(); const categories = Array.from(allCategories).sort();
const tags = Array.from(allTags).map(tag => `#${tag}`).sort(); const tags = Array.from(allTags).map(tag => `# ${tag}`).sort();
--- ---
@@ -98,7 +101,7 @@ const tags = Array.from(allTags).map(tag => `#${tag}`).sort();
</h3> </h3>
<div class="space-y-2"> <div class="space-y-2">
{categories.map((category) => ( {categories.map((category) => (
<a href={`/zh/blog/categories/${category.toLowerCase()}`} <a href={`/zh/blog/categories/${encodeURIComponent(category.toLowerCase())}`}
class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200"> class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200">
{category} {category}
</a> </a>
@@ -116,7 +119,7 @@ const tags = Array.from(allTags).map(tag => `#${tag}`).sort();
</h3> </h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{tags.map((tag) => ( {tags.map((tag) => (
<a href={`/zh/blog/tags/${tag.slice(1).toLowerCase()}`} <a href={`/zh/blog/tags/${encodeURIComponent(tag.slice(2).toLowerCase())}`}
class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200"> class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200">
{tag} {tag}
</a> </a>

View File

@@ -4,7 +4,10 @@ description: "探索 React Hooks 在函数组件中管理状态和副作用的
image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400&h=250&fit=crop&crop=center" image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400&h=250&fit=crop&crop=center"
date: "2025年5月10日" date: "2025年5月10日"
readTime: "5分钟阅读" readTime: "5分钟阅读"
tags: ["React", "JavaScript", "前端"] tags: ["React", "JavaScript", "前端开发"]
tagId: ["react", "javascript", "frontend"]
category: ["React", "前端"]
categoryId: ["react", "frontend"]
slug: "mastering-react-hooks" slug: "mastering-react-hooks"
layout: "../../../../layouts/BlogPostLayout.astro" layout: "../../../../layouts/BlogPostLayout.astro"
--- ---

View File

@@ -4,7 +4,7 @@ description: "掌握 TypeScript 在企业级应用中的高级技巧,提升代
image: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=400&h=250&fit=crop&crop=center" image: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=400&h=250&fit=crop&crop=center"
date: "2025年4月15日" date: "2025年4月15日"
readTime: "8分钟阅读" readTime: "8分钟阅读"
tags: ["TypeScript", "架构", "最佳实践"] tags: ["TypeScript", "JavaScript", "Architecture"]
slug: "typescript-best-practices" slug: "typescript-best-practices"
layout: "../../../../layouts/BlogPostLayout.astro" layout: "../../../../layouts/BlogPostLayout.astro"
--- ---

View File

@@ -9,11 +9,18 @@ import { type BlogPost } from '@/types';
export async function getStaticPaths() { export async function getStaticPaths() {
const allPosts = await Astro.glob('../posts/*.md'); const allPosts = await Astro.glob('../posts/*.md');
// 收集所有标签 // 收集所有标签ID或标签
const uniqueTags = new Set<string>(); const uniqueTags = new Set<string>();
allPosts.forEach(post => { allPosts.forEach(post => {
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) { // 优先使用 tagId 作为路由标识符
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
post.frontmatter.tagId.forEach(tagId => {
if (tagId) uniqueTags.add(tagId.toLowerCase());
});
}
// 如果没有 tagId则使用 tags 作为后备
else if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
post.frontmatter.tags.forEach(tag => { post.frontmatter.tags.forEach(tag => {
if (tag) uniqueTags.add(tag.toLowerCase()); if (tag) uniqueTags.add(tag.toLowerCase());
}); });
@@ -45,8 +52,14 @@ const allPosts = await Astro.glob('../posts/*.md');
// 处理博客文章数据 // 处理博客文章数据
const blogPosts: BlogPost[] = allPosts const blogPosts: BlogPost[] = allPosts
.filter(post => { .filter(post => {
// 检查文章是否包含当前标签 // 优先检查文章是否包含当前标签ID
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) { 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 => return post.frontmatter.tags.some(postTag =>
postTag.toLowerCase() === decodedTag.toLowerCase() postTag.toLowerCase() === decodedTag.toLowerCase()
); );
@@ -102,18 +115,22 @@ allPosts.forEach(post => {
if (postTag) allTags.add(postTag); if (postTag) allTags.add(postTag);
}); });
} }
// 同时收集标签ID用于内部路由
if (post.frontmatter?.tagId && Array.isArray(post.frontmatter.tagId)) {
// 这里我们不添加到 allTags 中,因为 tagId 只用于路由,不用于显示
}
}); });
// 转换为数组并排序 // 转换为数组并排序
const categories = Array.from(allCategories).sort(); const categories = Array.from(allCategories).sort();
const tags = Array.from(allTags).map(postTag => `#${postTag}`).sort(); const tags = Array.from(allTags).map(postTag => `# ${postTag}`).sort();
// 获取当前标签的格式化名称(首字母大写) // 获取当前标签的格式化名称(首字母大写)
const formattedTag = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1); const formattedTag = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
// 动态生成页面标题和描述 // 动态生成页面标题和描述
const pageTitle = `#${formattedTag} - 博客 | Joy Zhao`; const pageTitle = `# ${formattedTag} - 博客 | Joy Zhao`;
const pageDescription = `浏览带有 #${formattedTag} 标签的文章。深入了解我关于 ${formattedTag} 及相关主题的想法。`; const pageDescription = `浏览带有 # ${formattedTag} 标签的文章。深入了解我关于 ${formattedTag} 及相关主题的想法。`;
--- ---
<BlogLayout title={pageTitle} description={pageDescription}> <BlogLayout title={pageTitle} description={pageDescription}>
@@ -122,10 +139,10 @@ const pageDescription = `浏览带有 #${formattedTag} 标签的文章。深入
<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">#{formattedTag}</span> 标签: <span class="text-purple-500"># {formattedTag}</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">
浏览带有 #{formattedTag} 标签的文章。找到 {sortedBlogPosts.length} 篇文章。 浏览带有 # {formattedTag} 标签的文章。找到 {sortedBlogPosts.length} 篇文章。
</p> </p>
</div> </div>
</div> </div>
@@ -163,7 +180,7 @@ const pageDescription = `浏览带有 #${formattedTag} 标签的文章。深入
</h3> </h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{tags.map((tagItem) => { {tags.map((tagItem) => {
const tagName = tagItem.slice(1).toLowerCase(); const tagName = tagItem.slice(2).toLowerCase();
const isCurrentTag = tagName === decodedTag.toLowerCase(); const isCurrentTag = tagName === decodedTag.toLowerCase();
return ( return (
<a href={`/zh/blog/tags/${encodeURIComponent(tagName)}`} <a href={`/zh/blog/tags/${encodeURIComponent(tagName)}`}

View File

@@ -44,6 +44,9 @@ export interface BlogPost {
image: string; image: string;
slug: string; slug: string;
tags: string[]; tags: string[];
tagId?: string[];
category?: string[];
categoryId?: string[];
date: string; date: string;
readTime: string; readTime: string;
url?: string; url?: string;
@@ -59,7 +62,9 @@ export interface FrontmatterProps {
date?: string; // Alternative field name for publish date date?: string; // Alternative field name for publish date
author?: string; author?: string;
tags?: string[]; tags?: string[];
tagId?: string[]; // 标签唯一标识符,用于多语言环境下的标签路由
category?: string | string[]; category?: string | string[];
categoryId?: string[] | string; // 分类唯一标识符,用于多语言环境下的分类路由
readingTime?: number; readingTime?: number;
readTime?: string; // Alternative field name for reading time readTime?: string; // Alternative field name for reading time
} }