feat(blog): add dynamic tag and category pages for multilingual support
Implement dynamic routing for blog tags and categories in both English and Chinese Add sorting, filtering and sidebar navigation functionality Include responsive design with proper styling and fallbacks
This commit is contained in:
212
src/pages/blog/categories/[category].astro
Normal file
212
src/pages/blog/categories/[category].astro
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
import BlogLayout from '../../../layouts/BlogLayout.astro';
|
||||||
|
import BlogList from '../../../components/BlogList.tsx';
|
||||||
|
import { type Lang } from '@/i18n/utils';
|
||||||
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { type BlogPost } from '@/types';
|
||||||
|
|
||||||
|
// 为动态路由生成静态路径
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 收集所有分类
|
||||||
|
const uniqueCategories = 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) uniqueCategories.add(category.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为每个分类生成路径
|
||||||
|
return Array.from(uniqueCategories).map(category => ({
|
||||||
|
params: { category },
|
||||||
|
props: { category }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前语言环境
|
||||||
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
|
// 获取当前分类(从URL参数)
|
||||||
|
export interface Props {
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { category } = Astro.params;
|
||||||
|
const decodedCategory = category ? decodeURIComponent(category) : '';
|
||||||
|
|
||||||
|
|
||||||
|
// 使用Astro.glob读取所有博客文章
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 处理博客文章数据
|
||||||
|
const blogPosts: BlogPost[] = allPosts
|
||||||
|
.filter(post => {
|
||||||
|
// 检查文章是否属于当前分类
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 获取当前分类的格式化名称(首字母大写)
|
||||||
|
const formattedCategory = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1);
|
||||||
|
|
||||||
|
// 动态生成页面标题和描述
|
||||||
|
const pageTitle = `${formattedCategory} - Blog | Joy Zhao`;
|
||||||
|
const pageDescription = `Explore articles about ${formattedCategory}. Dive into my thoughts on ${formattedCategory} and related topics.`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BlogLayout title={pageTitle} description={pageDescription}>
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||||
|
<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">
|
||||||
|
Category: <span class="text-purple-500">{formattedCategory}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||||
|
Explore articles about {formattedCategory}. Found {sortedBlogPosts.length} article{sortedBlogPosts.length !== 1 ? 's' : ''}.
|
||||||
|
</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 -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l2 2-2 2m2-2H9m10 0V9M5 19l2-2-2-2m2 2H3m2 0v2"></path>
|
||||||
|
</svg>
|
||||||
|
Categories
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<a href={`/blog/categories/${cat.toLowerCase()}`}
|
||||||
|
class={`block transition-colors duration-200 ${cat.toLowerCase() === decodedCategory.toLowerCase() ? 'text-purple-500 font-medium' : 'text-muted-foreground hover:text-purple-500'}`}>
|
||||||
|
{cat}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
|
# Tags
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<a href={`/blog/tags/${tag.slice(1).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">
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blog Posts -->
|
||||||
|
<div class="lg:col-span-3">
|
||||||
|
{sortedBlogPosts.length > 0 ? (
|
||||||
|
<BlogList posts={sortedBlogPosts} lang="en" client:load />
|
||||||
|
) : (
|
||||||
|
<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="/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>
|
||||||
|
.line-clamp-3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
208
src/pages/blog/tags/[tag].astro
Normal file
208
src/pages/blog/tags/[tag].astro
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
import BlogLayout from '../../../layouts/BlogLayout.astro';
|
||||||
|
import BlogList from '../../../components/BlogList.tsx';
|
||||||
|
import { type Lang } from '@/i18n/utils';
|
||||||
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { type BlogPost } from '@/types';
|
||||||
|
|
||||||
|
// 为动态路由生成静态路径
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 收集所有标签
|
||||||
|
const uniqueTags = new Set<string>();
|
||||||
|
|
||||||
|
allPosts.forEach(post => {
|
||||||
|
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
||||||
|
post.frontmatter.tags.forEach(tag => {
|
||||||
|
if (tag) uniqueTags.add(tag.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为每个标签生成路径
|
||||||
|
return Array.from(uniqueTags).map(tag => ({
|
||||||
|
params: { tag },
|
||||||
|
props: { tag }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前语言环境
|
||||||
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
|
// 获取当前标签(从URL参数)
|
||||||
|
export interface Props {
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag } = Astro.params;
|
||||||
|
const decodedTag = tag ? decodeURIComponent(tag) : '';
|
||||||
|
|
||||||
|
|
||||||
|
// 使用Astro.glob读取所有博客文章
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 处理博客文章数据
|
||||||
|
const blogPosts: BlogPost[] = allPosts
|
||||||
|
.filter(post => {
|
||||||
|
// 检查文章是否包含当前标签
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签
|
||||||
|
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
||||||
|
post.frontmatter.tags.forEach(postTag => {
|
||||||
|
if (postTag) allTags.add(postTag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换为数组并排序
|
||||||
|
const categories = Array.from(allCategories).sort();
|
||||||
|
const tags = Array.from(allTags).map(postTag => `#${postTag}`).sort();
|
||||||
|
|
||||||
|
// 获取当前标签的格式化名称(首字母大写)
|
||||||
|
const formattedTag = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
|
||||||
|
|
||||||
|
// 动态生成页面标题和描述
|
||||||
|
const pageTitle = `#${formattedTag} - Blog | Joy Zhao`;
|
||||||
|
const pageDescription = `Explore articles tagged with #${formattedTag}. Dive into my thoughts on ${formattedTag} and related topics.`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BlogLayout title={pageTitle} description={pageDescription}>
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||||
|
<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">
|
||||||
|
Tag: <span class="text-purple-500">#{formattedTag}</span>
|
||||||
|
</h1>
|
||||||
|
<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' : ''}.
|
||||||
|
</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 -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l2 2-2 2m2-2H9m10 0V9M5 19l2-2-2-2m2 2H3m2 0v2"></path>
|
||||||
|
</svg>
|
||||||
|
Categories
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<a href={`/blog/categories/${cat.toLowerCase()}`}
|
||||||
|
class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200">
|
||||||
|
{cat}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
|
# Tags
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{tags.map((tagItem) => {
|
||||||
|
const tagName = tagItem.slice(1).toLowerCase();
|
||||||
|
const isCurrentTag = tagName === decodedTag.toLowerCase();
|
||||||
|
return (
|
||||||
|
<a href={`/blog/tags/${tagName}`}
|
||||||
|
class={`inline-block px-3 py-1 text-sm rounded-full transition-all duration-200 ${isCurrentTag ? 'bg-purple-500/20 text-purple-500 font-medium' : 'bg-muted text-muted-foreground hover:bg-purple-500/20 hover:text-purple-500'}`}>
|
||||||
|
{tagItem}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blog Posts -->
|
||||||
|
<div class="lg:col-span-3">
|
||||||
|
{sortedBlogPosts.length > 0 ? (
|
||||||
|
<BlogList posts={sortedBlogPosts} lang="en" client:load />
|
||||||
|
) : (
|
||||||
|
<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="/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>
|
||||||
|
.line-clamp-3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
212
src/pages/zh/blog/categories/[category].astro
Normal file
212
src/pages/zh/blog/categories/[category].astro
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
import BlogLayout from '../../../../layouts/BlogLayout.astro';
|
||||||
|
import BlogList from '../../../../components/BlogList.tsx';
|
||||||
|
import { type Lang } from '@/i18n/utils';
|
||||||
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { type BlogPost } from '@/types';
|
||||||
|
|
||||||
|
// 为动态路由生成静态路径
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 收集所有分类
|
||||||
|
const uniqueCategories = 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) uniqueCategories.add(category.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为每个分类生成路径
|
||||||
|
return Array.from(uniqueCategories).map(category => ({
|
||||||
|
params: { category },
|
||||||
|
props: { category }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前语言环境
|
||||||
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
|
// 获取当前分类(从URL参数)
|
||||||
|
export interface Props {
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { category } = Astro.params;
|
||||||
|
const decodedCategory = category ? decodeURIComponent(category) : '';
|
||||||
|
|
||||||
|
|
||||||
|
// 使用Astro.glob读取所有中文博客文章
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 处理博客文章数据
|
||||||
|
const blogPosts: BlogPost[] = allPosts
|
||||||
|
.filter(post => {
|
||||||
|
// 检查文章是否属于当前分类
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 获取当前分类的格式化名称(首字母大写)
|
||||||
|
const formattedCategory = decodedCategory.charAt(0).toUpperCase() + decodedCategory.slice(1);
|
||||||
|
|
||||||
|
// 动态生成页面标题和描述
|
||||||
|
const pageTitle = `${formattedCategory} - 博客 | 赵桂阳`;
|
||||||
|
const pageDescription = `探索关于${formattedCategory}的文章。深入了解我对${formattedCategory}和相关主题的思考。`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BlogLayout title={pageTitle} description={pageDescription}>
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||||
|
<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">
|
||||||
|
分类: <span class="text-purple-500">{formattedCategory}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||||
|
探索关于{formattedCategory}的文章。找到 {sortedBlogPosts.length} 篇文章。
|
||||||
|
</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">
|
||||||
|
<!-- 分类 -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l2 2-2 2m2-2H9m10 0V9M5 19l2-2-2-2m2 2H3m2 0v2"></path>
|
||||||
|
</svg>
|
||||||
|
分类
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<a href={`/zh/blog/categories/${cat.toLowerCase()}`}
|
||||||
|
class={`block transition-colors duration-200 ${cat.toLowerCase() === decodedCategory.toLowerCase() ? 'text-purple-500 font-medium' : 'text-muted-foreground hover:text-purple-500'}`}>
|
||||||
|
{cat}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签 -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
|
# 标签
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<a href={`/zh/blog/tags/${tag.slice(1).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">
|
||||||
|
{tag}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 博客文章 -->
|
||||||
|
<div class="lg:col-span-3">
|
||||||
|
{sortedBlogPosts.length > 0 ? (
|
||||||
|
<BlogList posts={sortedBlogPosts} lang="zh" client:load />
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
.line-clamp-3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
208
src/pages/zh/blog/tags/[tag].astro
Normal file
208
src/pages/zh/blog/tags/[tag].astro
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
import BlogLayout from '../../../../layouts/BlogLayout.astro';
|
||||||
|
import BlogList from '../../../../components/BlogList.tsx';
|
||||||
|
import { type Lang } from '@/i18n/utils';
|
||||||
|
import { defaultLang } from '@/i18n/ui';
|
||||||
|
import { type BlogPost } from '@/types';
|
||||||
|
|
||||||
|
// 为动态路由生成静态路径
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 收集所有标签
|
||||||
|
const uniqueTags = new Set<string>();
|
||||||
|
|
||||||
|
allPosts.forEach(post => {
|
||||||
|
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
||||||
|
post.frontmatter.tags.forEach(tag => {
|
||||||
|
if (tag) uniqueTags.add(tag.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 为每个标签生成路径
|
||||||
|
return Array.from(uniqueTags).map(tag => ({
|
||||||
|
params: { tag },
|
||||||
|
props: { tag }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前语言环境
|
||||||
|
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||||
|
|
||||||
|
// 获取当前标签(从URL参数)
|
||||||
|
export interface Props {
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag } = Astro.params;
|
||||||
|
const decodedTag = tag ? decodeURIComponent(tag) : '';
|
||||||
|
|
||||||
|
|
||||||
|
// 使用Astro.glob读取所有中文博客文章
|
||||||
|
const allPosts = await Astro.glob('../posts/*.md');
|
||||||
|
|
||||||
|
// 处理博客文章数据
|
||||||
|
const blogPosts: BlogPost[] = allPosts
|
||||||
|
.filter(post => {
|
||||||
|
// 检查文章是否包含当前标签
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签
|
||||||
|
if (post.frontmatter?.tags && Array.isArray(post.frontmatter.tags)) {
|
||||||
|
post.frontmatter.tags.forEach(postTag => {
|
||||||
|
if (postTag) allTags.add(postTag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换为数组并排序
|
||||||
|
const categories = Array.from(allCategories).sort();
|
||||||
|
const tags = Array.from(allTags).map(postTag => `#${postTag}`).sort();
|
||||||
|
|
||||||
|
// 获取当前标签的格式化名称(首字母大写)
|
||||||
|
const formattedTag = decodedTag.charAt(0).toUpperCase() + decodedTag.slice(1);
|
||||||
|
|
||||||
|
// 动态生成页面标题和描述
|
||||||
|
const pageTitle = `#${formattedTag} - 博客 | Joy Zhao`;
|
||||||
|
const pageDescription = `浏览带有 #${formattedTag} 标签的文章。深入了解我关于 ${formattedTag} 及相关主题的想法。`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BlogLayout title={pageTitle} description={pageDescription}>
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||||
|
<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">
|
||||||
|
标签: <span class="text-purple-500">#{formattedTag}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||||
|
浏览带有 #{formattedTag} 标签的文章。找到 {sortedBlogPosts.length} 篇文章。
|
||||||
|
</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 -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l2 2-2 2m2-2H9m10 0V9M5 19l2-2-2-2m2 2H3m2 0v2"></path>
|
||||||
|
</svg>
|
||||||
|
分类
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<a href={`/zh/blog/categories/${cat.toLowerCase()}`}
|
||||||
|
class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200">
|
||||||
|
{cat}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||||
|
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
|
# 标签
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{tags.map((tagItem) => {
|
||||||
|
const tagName = tagItem.slice(1).toLowerCase();
|
||||||
|
const isCurrentTag = tagName === decodedTag.toLowerCase();
|
||||||
|
return (
|
||||||
|
<a href={`/zh/blog/tags/${tagName}`}
|
||||||
|
class={`inline-block px-3 py-1 text-sm rounded-full transition-all duration-200 ${isCurrentTag ? 'bg-purple-500/20 text-purple-500 font-medium' : 'bg-muted text-muted-foreground hover:bg-purple-500/20 hover:text-purple-500'}`}>
|
||||||
|
{tagItem}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blog Posts -->
|
||||||
|
<div class="lg:col-span-3">
|
||||||
|
{sortedBlogPosts.length > 0 ? (
|
||||||
|
<BlogList posts={sortedBlogPosts} lang="zh" client:load />
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
.line-clamp-3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user