feat(taxonomy): add unified taxonomy page layout and utilities

refactor blog category and tag pages to use new TaxonomyPageLayout
add utility functions for processing taxonomy page data
pass lang prop to GlassHeader component
This commit is contained in:
joyzhao
2025-06-19 17:22:27 +08:00
parent 7951121c7f
commit 21fba08741
7 changed files with 262 additions and 383 deletions

View File

@@ -39,7 +39,7 @@ const lang = Astro.currentLocale as Lang || defaultLang;
<div class="absolute inset-0 bg-gradient-to-br from-purple-50/30 via-transparent to-blue-50/20 dark:from-purple-950/20 dark:via-transparent dark:to-blue-950/10"></div> <div class="absolute inset-0 bg-gradient-to-br from-purple-50/30 via-transparent to-blue-50/20 dark:from-purple-950/20 dark:via-transparent dark:to-blue-950/10"></div>
</div> </div>
<!-- Glass Header with navigation --> <!-- Glass Header with navigation -->
<GlassHeader client:load /> <GlassHeader lang={lang} client:load />
<!-- Main content with proper spacing for fixed header --> <!-- Main content with proper spacing for fixed header -->
<div class="pt-16"> <div class="pt-16">

View File

@@ -0,0 +1,95 @@
---
/**
* TaxonomyPageLayout component
* A unified layout for category and tag pages
*/
import BlogLayout from './BlogLayout.astro';
import BlogList from '../components/blog/BlogList.astro';
import CategoryCard from '../components/blog/CategoryCard.astro';
import TagCard from '../components/blog/TagCard.astro';
import { type Lang } from '@/i18n/utils';
import { defaultLang } from '@/i18n/ui';
import { type BlogPost } from '@/types';
interface Props {
lang: Lang;
title: string;
description?: string;
taxonomyType: 'category' | 'tag';
currentTaxonomy: string;
displayName?: string;
posts: BlogPost[];
categories: Map<string, string>;
tags: Map<string, string>;
}
const {
lang,
title,
taxonomyType,
currentTaxonomy,
displayName,
posts,
categories,
tags
} = Astro.props;
// Get localized text based on language and taxonomy type
const getLocalizedText = () => {
if (taxonomyType === 'category') {
return {
heading: lang === 'zh' ? '分类' : 'Category',
noResults: lang === 'zh' ? '该分类下没有文章' : 'No posts in this category',
};
} else {
return {
heading: lang === 'zh' ? '标签' : 'Tag',
noResults: lang === 'zh' ? '该标签下没有文章' : 'No posts with this tag',
};
}
};
const localizedText = getLocalizedText();
---
<BlogLayout title={title} description={Astro.props.description}>
<div class="container max-w-6xl px-4 py-8">
<!-- Header Section - Centered at the top -->
<div class="text-center mb-10">
<h1 class="text-4xl font-bold mb-3">
{displayName || currentTaxonomy}
</h1>
<p class="text-xl text-muted-foreground">
{localizedText.heading}: {posts.length} {lang === 'zh' ? '篇文章' : 'posts'}
</p>
</div>
<!-- Content Section - Sidebar and Main Content -->
<div class="flex flex-col md:flex-row gap-8">
<!-- Sidebar - Left on desktop, top on mobile -->
<div class="w-full md:w-80 space-y-6 md:flex-shrink-0">
<CategoryCard
lang={lang}
currentCategory={taxonomyType === 'category' ? currentTaxonomy : undefined}
/>
<TagCard
lang={lang}
currentTag={taxonomyType === 'tag' ? currentTaxonomy : undefined}
/>
</div>
<!-- Main Content -->
<div class="flex-1">
{posts.length > 0 ? (
<BlogList posts={posts} lang={lang} />
) : (
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-8 text-center border border-border">
<p class="text-lg text-muted-foreground">{localizedText.noResults}</p>
</div>
)}
</div>
</div>
</div>
</BlogLayout>

View File

@@ -1,12 +1,8 @@
--- ---
import BlogLayout from '../../../layouts/BlogLayout.astro'; import TaxonomyPageLayout from '../../../layouts/TaxonomyPageLayout.astro';
import BlogList from '../../../components/blog/BlogList.astro';
import CategoryCard from '../../../components/blog/CategoryCard.astro';
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 { getTaxonomyPageData } from '@/utils/blog-utils';
import { filterPostsByCategory, sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
// Generate static paths for dynamic routing // Generate static paths for dynamic routing
export async function getStaticPaths() { export async function getStaticPaths() {
@@ -56,35 +52,9 @@ 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 // Get taxonomy page data using the utility function
const allPosts = await import.meta.glob('../posts/*.md', { eager: true }); const { posts: sortedBlogPosts, categories, tags, allPosts } =
await getTaxonomyPageData(lang, { category: decodedCategory });
// Process blog post data
const blogPosts: BlogPost[] = Object.values(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',
};
});
// Filter posts by category
const filteredPosts = filterPostsByCategory(blogPosts, decodedCategory);
// Sort posts by date
const sortedBlogPosts = sortPostsByDate(filteredPosts);
// Find category name matching the current category ID // Find category name matching the current category ID
let categoryName = decodedCategory; let categoryName = decodedCategory;
@@ -118,70 +88,19 @@ Object.values(allPosts).forEach((post: any) => {
// Page title and description // Page title and description
const title = `${categoryName} - Blog | Joy Zhao`; const title = `${categoryName} - Blog | Joy Zhao`;
const description = `Explore articles about ${categoryName}. Dive into my thoughts on ${categoryName} and related topics.`; 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 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
// Get categories and tags for sidebar
const categories = extractCategories(allPostsArray);
const tags = extractTags(allPostsArray);
--- ---
<BlogLayout title={title} description={description}> <TaxonomyPageLayout
<main class="min-h-screen"> lang={lang}
<!-- Header Section --> title={title}
<div class="container mx-auto px-4 pt-24 pb-12"> description={description}
<div class="text-center mb-16"> taxonomyType="category"
<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"> currentTaxonomy={decodedCategory}
Category: <span class="text-purple-500">{categoryName}</span> displayName={categoryName}
</h1> posts={sortedBlogPosts}
<p class="text-xl text-muted-foreground max-w-3xl mx-auto"> categories={categories}
Explore articles about {categoryName}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}. tags={tags}
</p> />
</div>
</div>
<!-- Main Content -->
<div class="container mx-auto px-4 pb-20">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar -->
<div class="lg:col-span-1 space-y-8">
<!-- Categories card -->
<CategoryCard lang={lang} currentCategory={decodedCategory} />
<!-- Tags card -->
<TagCard lang={lang} />
</div>
<!-- Blog Posts -->
<div class="lg:col-span-3">
{sortedBlogPosts.length > 0 ? (
<BlogList posts={sortedBlogPosts} lang={lang} category={decodedCategory} />
) : (
<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>
<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={`/${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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Back to all posts
</a>
</div>
)}
</div>
</div>
</div>
</main>
</BlogLayout>
<style> <style>
.line-clamp-3 { .line-clamp-3 {

View File

@@ -1,12 +1,8 @@
--- ---
import BlogLayout from '../../../layouts/BlogLayout.astro'; import TaxonomyPageLayout from '../../../layouts/TaxonomyPageLayout.astro';
import BlogList from '../../../components/blog/BlogList.astro';
import CategoryCard from '../../../components/blog/CategoryCard.astro';
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 { getTaxonomyPageData } from '@/utils/blog-utils';
import { filterPostsByTag, sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
// Generate static paths for dynamic routing // Generate static paths for dynamic routing
export async function getStaticPaths() { export async function getStaticPaths() {
@@ -48,47 +44,9 @@ 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 // Get taxonomy page data using the utility function
const allPosts = await import.meta.glob('../posts/*.md', { eager: true }); const { posts: sortedBlogPosts, categories, tags, allPosts } =
await getTaxonomyPageData(lang, { tag: decodedTag });
// Process blog post data
const blogPosts: BlogPost[] = Object.values(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',
};
});
// Filter posts by tag
const filteredPosts = filterPostsByTag(blogPosts, decodedTag);
// Sort posts by date
const sortedBlogPosts = sortPostsByDate(filteredPosts);
// Extract categories and tags from all posts for sidebar
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 // Find tag name matching the current tag ID
let tagName = decodedTag; let tagName = decodedTag;
@@ -119,54 +77,17 @@ const title = `#${tagName} - Blog | Joy Zhao`;
const description = `Explore articles tagged with #${tagName}. Dive into my thoughts on ${tagName} and related topics.`; const description = `Explore articles tagged with #${tagName}. Dive into my thoughts on ${tagName} and related topics.`;
--- ---
<BlogLayout title={title} description={description}> <TaxonomyPageLayout
<main class="min-h-screen"> lang={lang}
<!-- Header Section --> title={title}
<section class="container mx-auto px-4 py-12"> description={description}
<div class="text-center mb-16"> taxonomyType="tag"
<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"> currentTaxonomy={decodedTag}
Tag: <span class="text-purple-500">#{tagName}</span> displayName={tagName}
</h1> posts={sortedBlogPosts}
<p class="text-xl text-muted-foreground max-w-3xl mx-auto"> categories={categories}
Explore articles tagged with #{tagName}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}. tags={tags}
</p> />
</div>
</section>
<!-- Content Section -->
<section class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar -->
<div class="lg:col-span-1 space-y-8">
<!-- Categories card -->
<CategoryCard lang={lang} />
<!-- Tags card -->
<TagCard lang={lang} currentTag={decodedTag} />
</div>
<!-- Main content -->
<div class="lg:col-span-3">
{sortedBlogPosts.length > 0 ? (
<BlogList posts={sortedBlogPosts} lang={lang} tag={decodedTag} />
) : (
<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>
<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={`/${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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
Back to all posts
</a>
</div>
)}
</div>
</div>
</section>
</main>
</BlogLayout>
<style> <style>
.line-clamp-3 { .line-clamp-3 {

View File

@@ -1,12 +1,8 @@
--- ---
import BlogLayout from '../../../../layouts/BlogLayout.astro'; import TaxonomyPageLayout from '../../../../layouts/TaxonomyPageLayout.astro';
import BlogList from '../../../../components/blog/BlogList.astro';
import CategoryCard from '../../../../components/blog/CategoryCard.astro';
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 { getTaxonomyPageData } from '@/utils/blog-utils';
import { filterPostsByCategory, sortPostsByDate, extractCategories, extractTags } from '@/utils/blog-utils';
// Generate static paths for dynamic routing // Generate static paths for dynamic routing
export async function getStaticPaths() { export async function getStaticPaths() {
@@ -56,47 +52,9 @@ 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 // Get taxonomy page data using the utility function
const allPosts = await import.meta.glob('../posts/*.md', { eager: true }); const { posts: sortedBlogPosts, categories, tags, allPosts } =
await getTaxonomyPageData(lang, { category: decodedCategory });
// Process blog post data
const blogPosts: BlogPost[] = Object.values(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',
};
});
// Filter posts by category
const filteredPosts = filterPostsByCategory(blogPosts, decodedCategory);
// Sort posts by date
const sortedBlogPosts = sortPostsByDate(filteredPosts);
// Extract categories and tags from all posts for sidebar
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 // Find category name matching the current category ID
let categoryName = decodedCategory; let categoryName = decodedCategory;
@@ -127,54 +85,17 @@ const title = `${categoryName} - 博客 | Joy Zhao`;
const description = `探索关于${categoryName}的文章。深入了解我对${categoryName}及相关主题的思考。`; const description = `探索关于${categoryName}的文章。深入了解我对${categoryName}及相关主题的思考。`;
--- ---
<BlogLayout title={title} description={description}> <TaxonomyPageLayout
<main class="min-h-screen"> lang={lang}
<!-- Header Section --> title={title}
<section class="container mx-auto px-4 py-12"> description={description}
<div class="text-center mb-16"> taxonomyType="category"
<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"> currentTaxonomy={decodedCategory}
分类: <span class="text-purple-500">{categoryName}</span> displayName={categoryName}
</h1> posts={sortedBlogPosts}
<p class="text-xl text-muted-foreground max-w-3xl mx-auto"> categories={categories}
探索关于{categoryName}的文章。找到 {sortedBlogPosts.length} 篇文章。 tags={tags}
</p> />
</div>
</section>
<!-- Content Section -->
<section class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar -->
<div class="lg:col-span-1 space-y-8">
<!-- Categories card -->
<CategoryCard lang={lang} currentCategory={decodedCategory} />
<!-- Tags card -->
<TagCard lang={lang} />
</div>
<!-- Main content -->
<div class="lg:col-span-3">
{sortedBlogPosts.length > 0 ? (
<BlogList posts={sortedBlogPosts} lang={lang} category={decodedCategory} />
) : (
<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>
<p class="text-muted-foreground mb-6">该分类下暂时没有文章。请稍后再来查看或浏览其他分类。</p>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
</svg>
返回所有文章
</a>
</div>
)}
</div>
</div>
</section>
</main>
</BlogLayout>
<style> <style>
.line-clamp-3 { .line-clamp-3 {

View File

@@ -1,12 +1,8 @@
--- ---
import BlogLayout from '../../../../layouts/BlogLayout.astro'; import TaxonomyPageLayout from '../../../../layouts/TaxonomyPageLayout.astro';
import BlogList from '../../../../components/blog/BlogList.astro';
import CategoryCard from '../../../../components/blog/CategoryCard.astro';
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 { getTaxonomyPageData } from '@/utils/blog-utils';
import { filterPostsByTag, sortPostsByDate, extractCategories, extractTags, getBlogBaseUrl } from '@/utils/blog-utils';
// 为动态路由生成静态路径 // 为动态路由生成静态路径
export async function getStaticPaths() { export async function getStaticPaths() {
@@ -48,47 +44,9 @@ export interface Props {
const { tag } = Astro.params; const { tag } = Astro.params;
const decodedTag = tag ? decodeURIComponent(tag) : ''; const decodedTag = tag ? decodeURIComponent(tag) : '';
// 使用import.meta.glob读取所有中文博客文章 // 获取分类页面数据,使用工具函数
const allPosts = await import.meta.glob('../posts/*.md', { eager: true }); const { posts: sortedBlogPosts, categories, tags, allPosts } =
await getTaxonomyPageData(lang, { tag: decodedTag });
// 处理博客文章数据
const blogPosts: BlogPost[] = Object.values(allPosts).map((post: any) => {
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 || [],
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 filteredPosts = filterPostsByTag(blogPosts, decodedTag);
// 使用工具函数按日期排序
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 tagName = ""; let tagName = "";
@@ -120,54 +78,17 @@ const pageTitle = `# ${tagName} - 博客 | Joy Zhao`;
const pageDescription = `浏览带有 # ${tagName} 标签的文章。深入了解我关于 ${tagName} 及相关主题的想法。`; const pageDescription = `浏览带有 # ${tagName} 标签的文章。深入了解我关于 ${tagName} 及相关主题的想法。`;
--- ---
<BlogLayout title={pageTitle} description={pageDescription}> <TaxonomyPageLayout
<main class="min-h-screen"> lang={lang}
<!-- 头部区域 --> title={pageTitle}
<div class="container mx-auto px-4 pt-24 pb-12"> description={pageDescription}
<div class="text-center mb-16"> taxonomyType="tag"
<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"> currentTaxonomy={decodedTag}
标签: <span class="text-purple-500"># {tagName}</span> displayName={tagName}
</h1> posts={sortedBlogPosts}
<p class="text-xl text-muted-foreground max-w-3xl mx-auto"> categories={categories}
浏览带有 # {tagName} 标签的文章。找到 {sortedBlogPosts.length} 篇文章。 tags={tags}
</p> />
</div>
</div>
<!-- 主要内容 -->
<div class="container mx-auto px-4 pb-20">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- 侧边栏 -->
<div class="lg:col-span-1 space-y-8">
<!-- 分类卡片 -->
<CategoryCard lang="zh" currentCategory="" />
<!-- 标签卡片 -->
<TagCard lang="zh" currentTag={decodedTag} />
</div>
<!-- 博客文章列表 -->
<div class="lg:col-span-3">
{sortedBlogPosts.length > 0 ? (
<BlogList posts={sortedBlogPosts} lang="zh" tag={decodedTag} />
) : (
<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>
<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">
<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>
</svg>
返回所有文章
</a>
</div>
)}
</div>
</div>
</div>
</main>
</BlogLayout>
<style> <style>
.line-clamp-3 { .line-clamp-3 {

View File

@@ -205,3 +205,105 @@ export function extractTags(posts: any[]): Map<string, string> {
export function getBlogBaseUrl(lang: Lang): string { export function getBlogBaseUrl(lang: Lang): string {
return lang === defaultLang ? '/blog' : `/${lang}/blog`; return lang === defaultLang ? '/blog' : `/${lang}/blog`;
} }
/**
* Process raw blog post data from import.meta.glob
* @param allPosts - Raw posts data from import.meta.glob
* @param lang - Current language
* @returns Processed blog posts array
*/
export function processRawBlogPosts(allPosts: Record<string, any>[] | any[], lang: Lang): BlogPost[] {
// Process blog post data
const posts: BlogPost[] = allPosts.map((post: any) => {
const slug = post.url?.split('/').filter(Boolean).pop() || '';
// Default image if not specified in frontmatter
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center";
// Default read time text based on language
const defaultReadTime = lang === 'zh' ? '5 分钟阅读' : '5 min read';
return {
title: post.frontmatter.title,
description: post.frontmatter.description || '',
image: post.frontmatter.image || defaultImage,
slug: slug,
tags: post.frontmatter.tags || [],
tagId: post.frontmatter.tagId || [],
category: Array.isArray(post.frontmatter.category) ? post.frontmatter.category : post.frontmatter.category ? [post.frontmatter.category] : [],
categoryId: Array.isArray(post.frontmatter.categoryId) ? post.frontmatter.categoryId : post.frontmatter.categoryId ? [post.frontmatter.categoryId] : [],
date: post.frontmatter.date || post.frontmatter.pubDate || '',
readTime: post.frontmatter.readTime || post.frontmatter.readingTime || defaultReadTime,
url: post.url || '',
};
});
return posts;
}
/**
* Extract post metadata for categories and tags
* @param posts - Array of blog posts
* @returns Array of post metadata
*/
export function extractPostMetadata(posts: any[]) {
return posts.map((post: any) => ({
category: post.frontmatter?.category || [],
categoryId: post.frontmatter?.categoryId || [],
tags: post.frontmatter?.tags || [],
tagId: post.frontmatter?.tagId || []
}));
}
/**
* Get all blog posts with processing for a taxonomy page (category or tag)
* @param lang - Current language
* @param options - Additional options for processing
* @returns Object containing processed posts and metadata
*/
export async function getTaxonomyPageData(lang: Lang, options?: {
category?: string;
tag?: string;
}) {
// Read all blog posts
// Note: import.meta.glob only accepts literal strings, not variables
let allPosts;
// Use specific glob patterns based on language
if (lang === 'zh') {
allPosts = await import.meta.glob('/src/pages/zh/blog/posts/*.{md,mdx}', { eager: true });
} else {
allPosts = await import.meta.glob('/src/pages/blog/posts/*.{md,mdx}', { eager: true });
}
// Process blog post data
const blogPosts = processRawBlogPosts(Object.values(allPosts), lang);
// Apply filters if specified
let filteredPosts = blogPosts;
if (options?.category) {
filteredPosts = filterPostsByCategory(filteredPosts, options.category);
}
if (options?.tag) {
filteredPosts = filterPostsByTag(filteredPosts, options.tag);
}
// Sort posts by date
const sortedBlogPosts = sortPostsByDate(filteredPosts);
// Extract categories and tags from all posts for sidebar
const allPostsMetadata = extractPostMetadata(Object.values(allPosts));
// Get categories and tags for sidebar
const categories = extractCategories(allPostsMetadata);
const tags = extractTags(allPostsMetadata);
return {
posts: sortedBlogPosts,
categories,
tags,
allPosts
};
}