initial commit
This commit is contained in:
32
src/pages/blog/[...slug].astro
Normal file
32
src/pages/blog/[...slug].astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||
import PostLayout from '../../layouts/PostLayout.astro';
|
||||
import Counter from '../../components/Counter.vue';
|
||||
import TypstBlock from '../../components/TypstBlock.astro';
|
||||
|
||||
// MDX 组件映射
|
||||
const mdxComponents = {
|
||||
Counter,
|
||||
TypstBlock,
|
||||
};
|
||||
|
||||
// 生成所有文章的静态路径
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug || post.id },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
interface Props {
|
||||
post: CollectionEntry<'blog'>;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<PostLayout post={post}>
|
||||
<Content components={mdxComponents} />
|
||||
</PostLayout>
|
||||
120
src/pages/blog/index.astro
Normal file
120
src/pages/blog/index.astro
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
// 获取所有非草稿文章
|
||||
const allPosts = await getCollection('blog', ({ data }) => {
|
||||
return !data.draft;
|
||||
});
|
||||
|
||||
// 按日期排序
|
||||
const sortedPosts = allPosts.sort((a, b) =>
|
||||
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||
);
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout title="博客文章 - NovaBlog">
|
||||
<div class="py-12">
|
||||
<div class="content-width">
|
||||
<!-- Page Header -->
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-4">博客文章</h1>
|
||||
<p class="text-foreground/60">探索技术,记录成长</p>
|
||||
</div>
|
||||
|
||||
{sortedPosts.length > 0 ? (
|
||||
<div class="max-w-4xl mx-auto space-y-8">
|
||||
{sortedPosts.map((post) => (
|
||||
<article class="card group">
|
||||
<a href={`/blog/${post.slug || post.id}`} class="block">
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<!-- 封面图 -->
|
||||
{post.data.heroImage ? (
|
||||
<div class="md:w-48 flex-shrink-0">
|
||||
<div class="aspect-video md:aspect-square rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={post.data.heroImage}
|
||||
alt={post.data.heroAlt || post.data.title}
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="md:w-48 flex-shrink-0 hidden md:block">
|
||||
<div class="aspect-square rounded-lg bg-gradient-to-br from-primary-100 to-accent/20 dark:from-primary-900 dark:to-accent/10 flex items-center justify-center">
|
||||
<svg class="w-12 h-12 text-primary-300 dark:text-primary-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 标签 -->
|
||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
||||
{post.data.category && (
|
||||
<span class="tag text-xs bg-primary-500 text-white">
|
||||
{post.data.category}
|
||||
</span>
|
||||
)}
|
||||
{(post.data.tags || []).slice(0, 3).map((tag: string) => (
|
||||
<span class="tag text-xs">#{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h2 class="text-xl font-semibold mb-2 group-hover:text-primary-500 transition-colors">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
|
||||
<!-- 描述 -->
|
||||
{post.data.description && (
|
||||
<p class="text-foreground/60 text-sm mb-4 line-clamp-2">
|
||||
{post.data.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<!-- 元信息 -->
|
||||
<div class="flex items-center gap-4 text-xs text-foreground/40">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
{post.data.author}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="text-center py-16 text-foreground/40">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p class="mb-2">暂无文章</p>
|
||||
<p class="text-sm">请在 src/content/blog/ 目录下添加 Markdown 文件</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
197
src/pages/index.astro
Normal file
197
src/pages/index.astro
Normal file
@@ -0,0 +1,197 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
// 获取最新的博客文章
|
||||
const allPosts = await getCollection('blog', ({ data }) => {
|
||||
return !data.draft;
|
||||
});
|
||||
|
||||
// 按日期排序
|
||||
const sortedPosts = allPosts.sort((a, b) =>
|
||||
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||
);
|
||||
|
||||
// 获取前6篇作为精选
|
||||
const featuredPosts = sortedPosts.slice(0, 6);
|
||||
|
||||
// 获取所有标签
|
||||
const allTags = [...new Set(allPosts.flatMap(post => post.data.tags || []))];
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout title="NovaBlog - 极简程序员博客">
|
||||
<!-- Hero Section -->
|
||||
<section class="py-20 md:py-32 bg-gradient-to-br from-primary-50 via-background to-accent/10 dark:from-primary-900/20 dark:to-accent/5">
|
||||
<div class="content-width text-center">
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold mb-6">
|
||||
<span class="gradient-text">NovaBlog</span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-foreground/60 max-w-2xl mx-auto mb-8">
|
||||
一个极简、高效的程序员博客系统
|
||||
</p>
|
||||
<p class="text-foreground/50 max-w-xl mx-auto mb-10">
|
||||
支持 MDX 动态组件、Typst 学术排版,静态渲染 + 轻量级微服务架构,极致性能与优雅排版的完美结合
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<a href="/blog" class="btn-primary">
|
||||
浏览文章
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/about" class="btn-secondary">
|
||||
了解更多
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Posts -->
|
||||
<section class="py-16">
|
||||
<div class="content-width">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h2 class="text-2xl md:text-3xl font-bold">最新文章</h2>
|
||||
<a href="/blog" class="text-primary-500 hover:text-primary-600 transition-colors flex items-center gap-1">
|
||||
查看全部
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{featuredPosts.length > 0 ? (
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{featuredPosts.map((post) => (
|
||||
<article class="card group cursor-pointer">
|
||||
<a href={`/blog/${post.slug || post.id}`}>
|
||||
{/* 封面图 */}
|
||||
{post.data.heroImage ? (
|
||||
<div class="aspect-video rounded-lg overflow-hidden mb-4 -mt-6 -mx-6 w-[calc(100%+3rem)]">
|
||||
<img
|
||||
src={post.data.heroImage}
|
||||
alt={post.data.heroAlt || post.data.title}
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="aspect-video rounded-lg bg-gradient-to-br from-primary-100 to-accent/20 dark:from-primary-900 dark:to-accent/10 mb-4 -mt-6 -mx-6 w-[calc(100%+3rem)] flex items-center justify-center">
|
||||
<svg class="w-12 h-12 text-primary-300 dark:text-primary-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- 分类 -->
|
||||
{post.data.category && (
|
||||
<span class="tag text-xs mb-3">
|
||||
{post.data.category}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<!-- 标题 -->
|
||||
<h3 class="text-lg font-semibold mb-2 group-hover:text-primary-500 transition-colors line-clamp-2">
|
||||
{post.data.title}
|
||||
</h3>
|
||||
|
||||
<!-- 描述 -->
|
||||
{post.data.description && (
|
||||
<p class="text-foreground/60 text-sm mb-4 line-clamp-2">
|
||||
{post.data.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<!-- 元信息 -->
|
||||
<div class="flex items-center justify-between text-xs text-foreground/40">
|
||||
<span>{formatDate(post.data.pubDate)}</span>
|
||||
<span>{post.data.author}</span>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="text-center py-16 text-foreground/40">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p>暂无文章,请在 src/content/blog/ 目录下添加 Markdown 文件</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tags Section -->
|
||||
<section class="py-16 bg-muted/30">
|
||||
<div class="content-width">
|
||||
<h2 class="text-2xl md:text-3xl font-bold mb-8 text-center">标签云</h2>
|
||||
{allTags.length > 0 ? (
|
||||
<div class="flex flex-wrap justify-center gap-3">
|
||||
{allTags.map((tag: string) => (
|
||||
<a href={`/tags/${tag}`} class="tag hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors">
|
||||
#{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-center text-foreground/40">暂无标签</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="py-16">
|
||||
<div class="content-width">
|
||||
<h2 class="text-2xl md:text-3xl font-bold mb-12 text-center">核心特性</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<!-- Feature 1 -->
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">极致性能</h3>
|
||||
<p class="text-foreground/60 text-sm">
|
||||
静态渲染 + Islands 架构,Zero-JS 默认,极致的加载速度
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">MDX 支持</h3>
|
||||
<p class="text-foreground/60 text-sm">
|
||||
在 Markdown 中嵌入 Vue/React 组件,实现丰富的交互效果
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3 -->
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2">低资源占用</h3>
|
||||
<p class="text-foreground/60 text-sm">
|
||||
Go 后端 + SQLite,2C1G 小鸡也能流畅运行
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
36
src/pages/login.astro
Normal file
36
src/pages/login.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import LoginForm from '../components/LoginForm.vue';
|
||||
---
|
||||
|
||||
<BaseLayout title="登录 - NovaBlog">
|
||||
<main class="login-page">
|
||||
<h1 class="page-title">欢迎回来</h1>
|
||||
<p class="page-subtitle">登录您的账户以发表评论和管理内容</p>
|
||||
<LoginForm client:visible />
|
||||
</main>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.login-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
</task_progress>
|
||||
</write_to_file>
|
||||
102
src/pages/tags/[tag].astro
Normal file
102
src/pages/tags/[tag].astro
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
// 生成所有标签的静态路径
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
const allTags = [...new Set(posts.flatMap((post) => post.data.tags || []))];
|
||||
return allTags.map((tag) => ({
|
||||
params: { tag },
|
||||
}));
|
||||
}
|
||||
|
||||
// 获取当前标签
|
||||
const { tag } = Astro.params;
|
||||
|
||||
// 获取该标签下的所有文章
|
||||
const allPosts = await getCollection('blog', ({ data }) => {
|
||||
return !data.draft && (data.tags || []).includes(tag || '');
|
||||
});
|
||||
|
||||
// 按日期排序
|
||||
const sortedPosts = allPosts.sort((a, b) =>
|
||||
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
|
||||
);
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
---
|
||||
|
||||
<BaseLayout title={`#${tag} - NovaBlog`}>
|
||||
<div class="py-12">
|
||||
<div class="content-width">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-4">
|
||||
<span class="gradient-text">#{tag}</span>
|
||||
</h1>
|
||||
<p class="text-foreground/60">{sortedPosts.length} 篇文章</p>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto space-y-8">
|
||||
{sortedPosts.map((post) => (
|
||||
<article class="card group">
|
||||
<a href={`/blog/${post.slug || post.id}`} class="block">
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<!-- 封面图 -->
|
||||
{post.data.heroImage ? (
|
||||
<div class="md:w-48 flex-shrink-0">
|
||||
<div class="aspect-video md:aspect-square rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={post.data.heroImage}
|
||||
alt={post.data.heroAlt || post.data.title}
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 标题 -->
|
||||
<h2 class="text-xl font-semibold mb-2 group-hover:text-primary-500 transition-colors">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
|
||||
<!-- 描述 -->
|
||||
{post.data.description && (
|
||||
<p class="text-foreground/60 text-sm mb-4 line-clamp-2">
|
||||
{post.data.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<!-- 元信息 -->
|
||||
<div class="flex items-center gap-4 text-xs text-foreground/40">
|
||||
<span>{formatDate(post.data.pubDate)}</span>
|
||||
<span>{post.data.author}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-12">
|
||||
<a href="/tags" class="btn-secondary">
|
||||
<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" />
|
||||
</svg>
|
||||
返回标签列表
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
54
src/pages/tags/index.astro
Normal file
54
src/pages/tags/index.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
// 获取所有文章
|
||||
const allPosts = await getCollection('blog', ({ data }) => {
|
||||
return !data.draft;
|
||||
});
|
||||
|
||||
// 统计标签
|
||||
const tagMap = new Map<string, number>();
|
||||
allPosts.forEach((post) => {
|
||||
(post.data.tags || []).forEach((tag: string) => {
|
||||
tagMap.set(tag, (tagMap.get(tag) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
// 按文章数量排序
|
||||
const sortedTags = [...tagMap.entries()].sort((a, b) => b[1] - a[1]);
|
||||
---
|
||||
|
||||
<BaseLayout title="标签 - NovaBlog">
|
||||
<div class="py-12">
|
||||
<div class="content-width">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-3xl md:text-4xl font-bold mb-4">标签</h1>
|
||||
<p class="text-foreground/60">按标签浏览文章</p>
|
||||
</div>
|
||||
|
||||
{sortedTags.length > 0 ? (
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{sortedTags.map(([tag, count]) => (
|
||||
<a
|
||||
href={`/tags/${tag}`}
|
||||
class="card px-6 py-4 flex items-center gap-3 hover:border-primary-500 transition-colors"
|
||||
>
|
||||
<span class="text-lg font-medium">#{tag}</span>
|
||||
<span class="text-sm text-foreground/40">{count} 篇文章</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="text-center py-16 text-foreground/40">
|
||||
<svg class="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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" />
|
||||
</svg>
|
||||
<p>暂无标签</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user