这个博客最早是一个 Hexo 主题,Pug 模板、node-sass 编译、jQuery 驱动。它很好看,但构建链已经跑不动了:node-sass@8 在 Node 20 以上直接装不上,hexo-renderer-pug 五年没有更新,评论区用的多说早在 2017 年就关停了。
于是我把它整个搬到了 Astro。
为什么是 Astro
选型时我比较了四个方案:
| 方案 | 优点 | 代价 |
|---|---|---|
| 留在 Hexo | 迁移成本最低 | 构建链需要自己兜底,插件生态老化 |
| Eleventy | 官方 Pug 插件,模板几乎原样迁移 | 分页、分类、RSS 要自己拼 |
| Astro | 生态最活跃,SCSS/TS 一等公民,内容集合有类型 | 模板要改写 |
| Hugo | 构建最快 | Go 模板,视觉还原成本高 |
最后选 Astro,核心原因是内容集合带类型:frontmatter 写错字段,构建时就会报错,而不是上线后才发现某篇文章没有日期。
内容模型
src/content.config.ts 里定义两个集合。用 glob loader 读取 src/content/posts/:
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { glob } from 'astro/loaders';
const posts = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
updated: z.coerce.date().optional(),
desc: z.string().optional(),
tags: z.array(z.string()).optional().default([]),
draft: z.boolean().optional().default(false),
toc: z.boolean().optional().default(false),
}),
});几个细节值得说:
z.coerce.date()让2026-08-14这种字符串自动变成Date,不必在 frontmatter 里写 ISO 时间。draft: true的文章在getSortedPosts()里被过滤,不会进列表、RSS 和站点地图。- 标签是数组,不做层级——分类和标签在结构上是重复的,我只要一个。
模板迁移
Pug 的 extends / block 对应 Astro 的布局组件和 <slot />。真正需要重写的是数据获取方式:
---
import { getSortedPosts } from '../lib/posts';
const posts = await getSortedPosts();
---
<ul>
{posts.map((post) => (
<li><a href={`/posts/${toSlug(post.id)}/`}>{post.data.title}</a></li>
))}
</ul>有三处最容易出错:
- 分页路由必须带参数。Astro 7 的
paginate()要求文件名叫[...page].astro或[page].astro,写成index.astro会直接报错。 - slug 不可逆。
post.id是文件名,转成 URL 之后没法反推,所以详情页用getStaticPaths把完整对象放进props。 - 客户端路由会重复绑定。脚本必须挂在
astro:page-load上,并且用单例清理旧监听器。
构建链
原来 npm run build 是 node-sass,现在是 astro build,SCSS 直接由 Astro 编译。字体也从 Google Fonts 外链改成了自托管子集:
@font-face {
font-family: "Manrope";
font-weight: 400;
font-display: swap;
src: url("/fonts/manrope-latin-400-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}中文不加载 webfont,全部走系统字体栈。这一个决定把字体体积从几 MB 压到了 100KB 以内。
结果
- 构建:一次
astro build完成 33 个页面,约 0.5 秒 - 首屏:1 个 CSS + 2 个小 chunk,没有框架运行时
- 类型:
astro check零错误 - 旧版本:完整保留在 git tag 里,随时可以
git checkout回去对照
重构最花时间的不是写代码,而是搞清楚原来那行代码为什么那么写。Pug 模板里每一处奇怪的写法背后都有一个当年的决定,只有读懂它,才敢删掉它。