my-blog 博客列表页改造

目标:在
my-blog上做一套完整的/blog/文章列表体系,提供两条可选路线:
- 路线一(复刻 mare):竖向堆叠的横向条目卡片(左蓝竖条 + 标题 + 描述 + 日期 + 标签 + 右侧箭头)。见第一~六章。
- 路线二(纯竖向列表,如 pudding):无卡片,
日期 ↔ 标题极简排列,或左侧虚线时间线(圆点 + 标题 hover)。见第七章。两条路线都支持分页条(第八章,复用第四章分页器);并额外提供标签页
/blog/tags/(标签云 + 单标签筛选页,第九章),标签链接统一指向/blog/tags/<tag>(第九章第 4 节)。本文只给方案,不修改任何
my-blog项目文件。所有改动落在新建文件上,不影响现有首页与文章页。
一、mare 是怎么实现的(原理)
| 层 | 文件 | 作用 |
|---|---|---|
| 路由 | src/pages/blog/[...page].astro |
用 paginate() 把文章按 pubDate 倒序分页,每页 siteConfig.postsPerPage(默认 6)篇 |
| 卡片 | src/components/PostCard.astro |
渲染一张横向条目卡片(article.flex.items-stretch.gap-4):左内容右箭头 |
| 排列 | 列表页用 <div class="flex flex-col gap-6"> 把卡片竖向堆叠 |
不是网格方块,是“一行一行”的列表 |
| 分页条 | 列表页底部 <nav> 渲染上一页 / 页码 / 下一页 |
用 page.url.prev / next、pageLinks |
卡片外观要点(PostCard.astro):
- 整卡:
rounded-lg bg-white p-5 shadow-sm(圆角白底浅阴影) - 标题:左侧
h-8 w-1 rounded-full bg-blue-500蓝竖条 + 标题文字,hover 变蓝 + 右侧 chevron 箭头淡入 - 描述:
line-clamp-2最多两行 - 日期:灰色圆角徽章(
bg-gray-50),带日历图标 - 标签:
bg-blue-50/70浅蓝 chips,hover 变实心蓝 - 右侧:一个竖长箭头按钮,整行可点
二、my-blog 现状对比
| 项 | my-blog 现状 | 需要做什么 |
|---|---|---|
/blog/ 列表页 |
不存在(只有 blog/[...slug].astro 文章详情页) |
新建 src/pages/blog/index.astro(或 [...page].astro) |
| 文章列表卡片 | 无 | 新建 src/components/PostCard.astro |
| 内容字段 | title / description / pubDate / tags 已有 |
直接可用 |
| 图标 | 用 astro-icon 的 lucide 图标 |
my-blog 未装 astro-icon → 改用内联 SVG(零依赖) |
| 暗色模式 | my-blog 无暗色模式 | 去掉 mare 里的 dark: 类(留着也无害,只是不生效) |
| 标签链接 | mare 指向 /blog/tags/... 标签页 |
my-blog 无标签页 → 暂用 /blog?tag=xxx 或 # |
| 文章 URL | blog/[...slug].astro 用 params:{slug:post.id} → /blog/<id> |
卡片链接写 /blog/${post.id} 即可 |
| 首页 | 默认 starter 模板 | 按你要求不动;可顺手加个“查看全部文章”链接到 /blog |
三、改动文件清单
| 序号 | 操作 | 文件 | 说明 |
|---|---|---|---|
| 1 | 新建 | src/pages/blog/index.astro |
列表页(方案 A 单页 / 方案 B 分页) |
| 2 | 新建 | src/components/PostCard.astro |
横向条目卡片(适配 my-blog:内联 SVG、无 dark) |
| 3 | (可选)改 | src/consts.ts |
加 POSTS_PER_PAGE 常量(仅方案 B 用) |
| 4 | (可选)改 | src/pages/index.astro |
加一个指向 /blog 的入口链接(首页不动布局) |
| 5 | 新建 | src/components/PostList.astro |
路线二纯列表组件(极简 / 时间线两种变体,无卡片) |
| 6 | 新建 | src/pages/blog/[...page].astro |
路线二 + 分页版列表页(paginate(),每页 8 篇) |
| 7 | 新建 | src/pages/blog/tags/index.astro |
标签页首页:列出全部标签 + 每标签文章数 |
| 8 | 新建 | src/pages/blog/tags/[tag].astro |
单标签页:按标签筛选,复用路线二列表 |
不涉及
astro.config.mjs、global.css、BlogPost.astro等现有文件,风险低。
四、具体实现
4.1 新建 src/components/PostCard.astro
这是核心。相比 mare 的版本:① 图标全部换成内联 SVG(不依赖 astro-icon);② 删掉所有
dark:类;③ 标签链接暂指向/blog?tag=xxx;④ 日期格式改中文。
---
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'blog'>;
}
const { post } = Astro.props;
---
<article class="flex items-stretch gap-4 rounded-lg bg-white p-5 shadow-sm">
<div class="mt-2.5 ml-2.5 flex min-w-0 flex-1 flex-col">
<a href={`/blog/${post.id}`} class="group/title">
<h2 class="flex items-center gap-3 text-2xl font-bold text-gray-800 transition group-hover/title:text-blue-500">
<span class="h-8 w-1 shrink-0 rounded-full bg-blue-500"></span>
<span>{post.data.title}</span>

</h2>
</a>
{post.data.description && (
<p class="my-2 line-clamp-2 text-sm text-gray-600">{post.data.description}</p>
)}
<time datetime={post.data.pubDate.toISOString()}
class="mr-auto flex inline-flex items-center gap-1 rounded-md bg-gray-50 p-2 text-xs font-medium text-gray-400">

<span>{post.data.pubDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
</time>
{post.data.tags.length > 0 && (
<div class="mt-4">
<ul class="flex flex-wrap gap-2">
{post.data.tags.map((tag: string) => (
<li>
<a href={`/blog?tag=${encodeURIComponent(tag)}`}
class="flex inline-flex min-h-5 items-center gap-1 rounded-md bg-blue-50/70 px-2.5 py-1.5 text-xs font-medium text-blue-600 no-underline transition-colors duration-200 hover:bg-blue-500 hover:text-white">

{tag}
</a>
</li>
))}
</ul>
</div>
)}
</div>
<a href={`/blog/${post.id}`} aria-label={`阅读:${post.data.title}`}
class="inline-flex shrink-0 items-center justify-center rounded-md border border-gray-200 px-1.5 py-5 text-gray-600 transition hover:bg-gray-100">

</a>
</article>
卡片背景说明:上面用了
bg-white。如果你的global.css里<body>背景是浅灰(主题变量--gray-light很浅),白卡片会有轻微对比,正常;若想跟主题更融,可改成bg-[rgb(var(--gray-light))]或直接使用透明背景(去掉bg-white)。
4.2 方案 A(推荐,简单):新建 src/pages/blog/index.astro(单页不分页)
适合文章数量不多(你目前 3 篇)的情况,全部列出,不用分页条。
---
import BaseHead from '../../components/BaseHead.astro';
import Footer from '../../components/Footer.astro';
import Header from '../../components/Header.astro';
import PostCard from '../../components/PostCard.astro';
import { getCollection } from 'astro:content';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
const posts = (await getCollection('blog'))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<!doctype html>
<html lang="zh-CN">
<head>
<BaseHead title={`${SITE_TITLE} · 文章`} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main class="mx-auto w-full max-w-3xl px-4 py-10">
<h1 class="mb-6 text-3xl font-bold">全部文章</h1>
<div class="flex flex-col gap-6">
{posts.map((post) => <PostCard post={post} />)}
</div>
</main>
<Footer />
</body>
</html>
容器用
max-w-3xl(约 768px)居中,和文章页.prose宽度接近,视觉统一。flex flex-col gap-6就是“竖向堆叠卡片”的关键。
4.3 方案 B(还原 mare,带分页条):src/pages/blog/[...page].astro
若你想和 mare 一样有“页码导航”,用 paginate():
---
import BaseHead from '../../components/BaseHead.astro';
import Footer from '../../components/Footer.astro';
import Header from '../../components/Header.astro';
import PostCard from '../../components/PostCard.astro';
import { getCollection, type CollectionEntry } from 'astro:content';
import type { GetStaticPaths, Page } from 'astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return paginate(posts, { pageSize: 6 }); // ← 每页篇数
}) satisfies GetStaticPaths;
type Props = { page: Page<CollectionEntry<'blog'>> };
const { page } = Astro.props;
const pageTitle = page.currentPage === 1 ? '全部文章' : `全部文章 - 第 ${page.currentPage} 页`;
const pageLinks = Array.from({ length: page.lastPage }, (_, i) => i + 1);
---
<!doctype html>
<html lang="zh-CN">
<head>
<BaseHead title={`${SITE_TITLE} · ${pageTitle}`} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main class="mx-auto w-full max-w-3xl px-4 py-10">
<h1 class="mb-6 text-3xl font-bold">{pageTitle}</h1>
<div class="flex flex-col gap-6">
{page.data.map((post) => <PostCard post={post} />)}
</div>
{page.lastPage > 1 && (
<nav aria-label="分页" class="mt-8 flex flex-wrap items-center justify-center gap-2 text-sm">
{page.url.prev ? (
<a href={page.url.prev} aria-label="上一页"
class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-700 transition hover:border-blue-600 hover:bg-blue-50 hover:text-blue-600">‹</a>
) : (
<span class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-gray-200 bg-gray-100 text-gray-400">‹</span>
)}
{pageLinks.map((n) => (
<a href={n === 1 ? '/blog' : `/blog/${n}`}
aria-current={page.currentPage === n ? 'page' : undefined}
class={`flex h-10 w-10 items-center justify-center rounded-lg border border-gray-200 text-sm ${page.currentPage === n ? 'bg-blue-600 font-medium text-white' : 'bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600'}`}>
{n}
</a>
))}
{page.url.next ? (
<a href={page.url.next} aria-label="下一页"
class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-700 transition hover:border-blue-600 hover:bg-blue-50 hover:text-blue-600">›</a>
) : (
<span class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-gray-200 bg-gray-100 text-gray-400">›</span>
)}
</nav>
)}
</main>
<Footer />
</body>
</html>
pageSize: 6可抽到consts.ts做POSTS_PER_PAGE(可选)。文章少于 6 篇时page.lastPage === 1,分页条不渲染,第一页就全显示——和单页效果一致,但保留了扩展能力。
4.4 (可选)在首页加入口链接
保留首页原布局,只是在合适位置加一句:
<p class="mt-6">
<a href="/blog" class="text-blue-600 underline">查看全部文章 →</a>
</p>
五、移植到 my-blog 的关键适配点
| 要点 | 说明 |
|---|---|
| 图标 | mare 用 astro-icon(Icon name="lucide:...")。my-blog 没装,所以上面全部改用内联 SVG,零依赖。若你愿意装 @astrojs/icon,也可照搬 mare 写法。 |
| 暗色模式 | mare 卡片有大量 dark: 类。my-blog 无暗色模式,这些类不会生效(无害),上面已删除以保持干净。 |
| 标签链接 | mare 指向 /blog/tags/<tag>(需建标签页)。my-blog 暂无,先写成 /blog?tag=<tag>(占位),以后做标签页再改。 |
| 日期格式 | mare 是英文 toLocaleString('en-US', ...)。上面改 toLocaleDateString('zh-CN', {month:'long'}) 显示“2026年10月10日”。想保留英文就改回去。 |
| Tailwind 类 | line-clamp-2、bg-blue-500、shadow-sm、rounded-lg 等都是 Tailwind v4 内置,my-blog 的 @tailwindcss/vite 开箱即用,不用额外配置。 |
| 文章 URL | blog/[...slug].astro 用 post.id 作 slug,所以卡片 href="/blog/${post.id}" 正确。若你的 id 含目录层级(如 2026/post),URL 会自动带上去,无需改。 |
| 卡片背景 | 见 4.1 末尾说明,按主题背景微调 bg-white。 |
六、验证
cd D:\blog\my-blog
npm run dev
打开 http://localhost:4321/blog,应看到:
- 竖向排列的横向卡片,每张有左蓝竖条 + 标题(hover 变蓝 + 箭头出现)+ 描述 + 日期 + 标签 + 右侧箭头按钮
- 点卡片或右侧箭头跳到对应文章
- 文章少时无需翻页;用方案 B 且文章 > 6 篇时出现分页条
npm run build 后 /blog/ 会作为静态页进 dist,照常部署到 VPS。
七、路线二:纯竖向列表(无卡片,如 pudding)
如果你不想要卡片方块,只想像 my-blog-pudding 那样按顺序竖向排成一条时间线,用下面这套。核心是一个可复用的 PostList.astro 组件,提供两种视觉:
- 7.1 极简列表:一行一个,
日期 ↔ 标题,靠border-b分隔,最干净。 - 7.2 时间线(pudding 风):左侧一条虚线 + 圆点,每个条目上方显示日期、下方是标题(hover 变主题色)。
7.0 新建 src/components/PostList.astro(两种变体合一)
用
variant切换:'simple'(7.1)或'timeline'(7.2,默认)。颜色全部走主题变量:var(--accent)(hex)用于主题色,rgb(var(--gray-*))(RGB 分量)用于灰阶;无dark:类。
---
import type { CollectionEntry } from 'astro:content';
interface Props {
posts: CollectionEntry<'blog'>[];
variant?: 'simple' | 'timeline';
}
const { posts, variant = 'timeline' } = Astro.props;
const fmt = (d: Date) =>
d.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
---
{
variant === 'simple' ? (
<ul class="divide-y divide-[rgb(var(--gray-light))]">
{posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}
class="group flex items-baseline justify-between gap-4 py-3 no-underline">
<span class="text-lg text-[rgb(var(--gray-dark))] transition group-hover:text-[var(--accent)]">
{post.data.title}
</span>
<time datetime={post.data.pubDate.toISOString()}
class="shrink-0 text-sm text-[rgb(var(--gray))]">{fmt(post.data.pubDate)}</time>
</a>
</li>
))}
</ul>
) : (
<ul class="relative ml-2 border-l border-dashed border-[rgb(var(--gray-light))]">
{posts.map((post) => (
<li class="relative pl-6 py-3">
<span class="absolute -left-[5px] top-6 h-2 w-2 rounded-full bg-[var(--accent)]"></span>
<a href={`/blog/${post.id}`} class="group no-underline">
<time datetime={post.data.pubDate.toISOString()}
class="block text-xs text-[rgb(var(--gray))] mb-1">{fmt(post.data.pubDate)}</time>
<span class="text-lg text-[rgb(var(--gray-dark))] transition group-hover:text-[var(--accent)]">
{post.data.title}
</span>
</a>
</li>
))}
</ul>
)
}
7.1 极简列表用法(落地文件:src/pages/blog/index.astro)
“列表页”就是 src/pages/blog/index.astro——和路线一方案 A(4.2)是同一个文件,走路线二就在这里用 PostList 取代 PostCard 即可。新建 / 修改这个文件:
---
import BaseHead from '../../components/BaseHead.astro';
import Footer from '../../components/Footer.astro';
import Header from '../../components/Header.astro';
import PostList from '../../components/PostList.astro';
import { getCollection } from 'astro:content';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
const posts = (await getCollection('blog'))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<!doctype html>
<html lang="zh-CN">
<head>
<BaseHead title={`${SITE_TITLE} · 文章`} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main class="mx-auto w-full max-w-3xl px-4 py-10">
<h1 class="mb-6 text-3xl font-bold">全部文章</h1>
<PostList posts={posts} variant="simple" />
</main>
<Footer />
</body>
</html>
关键就一行
<PostList posts={posts} variant="simple" />取代了路线一里的<PostCard post={post} />。文章少(你目前 3 篇)用这个单页最省事,无需分页条。二选一提醒:
blog/index.astro(本文件,单页)与第八章blog/[...page].astro(分页)都匹配/blog/,不要同时存在,否则 Astro 路由冲突。要分页就删掉本文件、用第八章;要单页就别建[...page].astro。
7.2 pudding 时间线用法
落地文件与 7.1 完全相同(src/pages/blog/index.astro),只是把那一行换成:
<PostList posts={posts} variant="timeline" />
(或分页版第八章里把 <PostList posts={page.data} variant="timeline" /> 保持即可,想要极简就改 variant="simple")。效果即左侧虚线时间线:日期在上、标题在下、hover 变主题色。
八、路线二 + 分页(src/pages/blog/[...page].astro)
把第七章的纯列表套进 paginate(),每页 8 篇,分页条直接复用 4.3 的结构(只是配色改成主题变量)。
---
import BaseHead from '../../components/BaseHead.astro';
import Footer from '../../components/Footer.astro';
import Header from '../../components/Header.astro';
import PostList from '../../components/PostList.astro';
import { getCollection, type CollectionEntry } from 'astro:content';
import type { GetStaticPaths, Page } from 'astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return paginate(posts, { pageSize: 8 }); // ← 路线二每页 8 篇
}) satisfies GetStaticPaths;
type Props = { page: Page<CollectionEntry<'blog'>> };
const { page } = Astro.props;
const pageTitle = page.currentPage === 1 ? '全部文章' : `全部文章 - 第 ${page.currentPage} 页`;
const pageLinks = Array.from({ length: page.lastPage }, (_, i) => i + 1);
---
<!doctype html>
<html lang="zh-CN">
<head>
<BaseHead title={`${SITE_TITLE} · ${pageTitle}`} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main class="mx-auto w-full max-w-3xl px-4 py-10">
<h1 class="mb-6 text-3xl font-bold">全部文章</h1>
<PostList posts={page.data} variant="timeline" />
{page.lastPage > 1 && (
<nav aria-label="分页" class="mt-8 flex flex-wrap items-center justify-center gap-2 text-sm">
{page.url.prev ? (
<a href={page.url.prev} aria-label="上一页"
class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-[rgb(var(--gray-light))] bg-white text-[rgb(var(--gray-dark))] transition hover:border-[var(--accent)] hover:bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] hover:text-[var(--accent)]">‹</a>
) : (
<span class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-[rgb(var(--gray-light))] bg-[rgb(var(--gray-light))] text-[rgb(var(--gray))]">‹</span>
)}
{pageLinks.map((n) => (
<a href={n === 1 ? '/blog' : `/blog/${n}`}
aria-current={page.currentPage === n ? 'page' : undefined}
class={`flex h-10 w-10 items-center justify-center rounded-lg border border-[rgb(var(--gray-light))] text-sm ${page.currentPage === n ? 'bg-[var(--accent)] font-medium text-white' : 'bg-white text-[rgb(var(--gray-dark))] hover:bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] hover:text-[var(--accent)]'}`}>
{n}
</a>
))}
{page.url.next ? (
<a href={page.url.next} aria-label="下一页"
class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-[rgb(var(--gray-light))] bg-white text-[rgb(var(--gray-dark))] transition hover:border-[var(--accent)] hover:bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] hover:text-[var(--accent)]">›</a>
) : (
<span class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-[rgb(var(--gray-light))] bg-[rgb(var(--gray-light))] text-[rgb(var(--gray))]">›</span>
)}
</nav>
)}
</main>
<Footer />
</body>
</html>
配色要点:高亮态用
bg-[var(--accent)](hex 直接当背景色没问题),hover 浅底用bg-[color-mix(in_srgb,var(--accent)_10%,transparent)](Tailwind 任意值里空格写成_);灰阶一律rgb(var(--gray-*))。文章 ≤ 8 篇时page.lastPage === 1,分页条不渲染,和单页一致。
九、标签页 /blog/tags/
9.1 路由优先级提醒
src/pages/blog/ 下现在同时有:
index.astro(路线一单页)或[...page].astro(路线二分页)tags/index.astro(标签首页)tags/[tag].astro(单标签)
Astro 静态路由优先级:具体路径 > 动态参数 > rest([...page])。所以 /blog/tags 和 /blog/tags/foo 会优先匹配 tags/*,不会被 [...page] 吃掉,无需额外处理。
9.2 新建 src/pages/blog/tags/index.astro(标签云 + 计数)
---
import BaseHead from '../../../components/BaseHead.astro';
import Footer from '../../../components/Footer.astro';
import Header from '../../../components/Header.astro';
import { getCollection } from 'astro:content';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../../consts';
const posts = await getCollection('blog');
const counts = new Map<string, number>();
for (const p of posts) for (const t of p.data.tags) counts.set(t, (counts.get(t) ?? 0) + 1);
const tags = [...counts.entries()].sort((a, b) => b[1] - a[1]);
---
<!doctype html>
<html lang="zh-CN">
<head>
<BaseHead title={`${SITE_TITLE} · 标签`} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main class="mx-auto w-full max-w-3xl px-4 py-10">
<h1 class="mb-6 text-3xl font-bold">全部标签</h1>
<div class="flex flex-wrap gap-3">
{tags.map(([tag, count]) => (
<a href={`/blog/tags/${encodeURIComponent(tag)}`}
class="inline-flex items-center gap-1.5 rounded-full border border-[rgb(var(--gray-light))] px-4 py-2 text-sm text-[rgb(var(--gray-dark))] no-underline transition hover:border-[var(--accent)] hover:text-[var(--accent)]">
{tag}<span class="text-xs text-[rgb(var(--gray))]">({count})</span>
</a>
))}
</div>
</main>
<Footer />
</body>
</html>
9.3 新建 src/pages/blog/tags/[tag].astro(单标签筛选页)
---
import BaseHead from '../../../components/BaseHead.astro';
import Footer from '../../../components/Footer.astro';
import Header from '../../../components/Header.astro';
import PostList from '../../../components/PostList.astro';
import { getCollection } from 'astro:content';
import type { GetStaticPaths } from 'astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../../consts';
export const getStaticPaths = (async () => {
const posts = await getCollection('blog');
const tags = new Set<string>();
posts.forEach((p) => p.data.tags.forEach((t) => tags.add(t)));
return [...tags].map((tag) => ({
params: { tag: encodeURIComponent(tag) },
props: {
tag,
posts: posts
.filter((p) => p.data.tags.includes(tag))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()),
},
}));
}) satisfies GetStaticPaths;
const { tag, posts } = Astro.props;
const decoded = decodeURIComponent(tag);
---
<!doctype html>
<html lang="zh-CN">
<head>
<BaseHead title={`${SITE_TITLE} · 标签:${decoded}`} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main class="mx-auto w-full max-w-3xl px-4 py-10">
<p class="mb-2 text-sm text-[rgb(var(--gray))]">
<a href="/blog/tags" class="no-underline text-[var(--accent)]">标签</a> /
</p>
<h1 class="mb-6 text-3xl font-bold">#{decoded}</h1>
<PostList posts={posts} variant="timeline" />
</main>
<Footer />
</body>
</html>
params.tag用encodeURIComponent(tag),这样中文/带空格的标签也能生成合法 URL;页面内decodeURIComponent还原显示。
9.4 把标签链接统一指向 /blog/tags/...
之前的占位链接(PostCard 里的 /blog?tag=...、路线二里的 #)全部改为真实标签页:
PostCard.astro(4.1)第 98–99 行:<a href={`/blog/tags/${encodeURIComponent(tag)}`} class="...">- 若
PostList也想显示标签(可选):在条目里加一组同样的<a href={/blog/tags/${encodeURIComponent(tag)}}>,样式沿用 9.2 的胶囊。
十、验证(路线二 + 分页 + 标签页)
cd D:\blog\my-blog
npm run dev
打开以下地址逐一确认:
| 地址 | 应看到 |
|---|---|
/blog |
路线二时间线列表(第一页,8 篇以内不分页) |
/blog/2、/blog/3 |
第 2、3 页;底部出现分页条,当前页高亮为主题色 |
/blog/tags |
全部标签胶囊,每个带文章数计数 |
/blog/tags/<某标签> |
该标签下的文章,用路线二列表呈现;面包屑 标签 / 可跳回标签首页 |
npm run build 后,/blog/、/blog/2...、/blog/tags/、/blog/tags/<tag> 全部作为静态页进 dist,照常部署到 VPS(标签页是 getStaticPaths 预渲染的,无需服务端逻辑)。