在 2026 年把静态站点从 Zola 迁移到 Astro
2026 年做静态站点,工具大致收敛成了几条清晰的路线。下面先横向看一圈,再聊聊把一次 Zola 站点迁移到 Astro 之后,实际赢下了什么,以及过程中那些具体的坑——代码高亮与语言支持、中文标题锚点、中文标签转拼音,以及 Tailwind 的接入。
横向看:2026 年的四种形态
Hugo、Zola、Eleventy、Astro 基本覆盖了内容站的四种形态:
| 工具 | 运行时 | 内容 / 模板模型 | 构建速度 | 定位 |
|---|---|---|---|---|
| Hugo | Go 单二进制 | Go 模板 | 极快 | 大站、主题生态 |
| Zola | Rust 单二进制 | Tera 模板(类 Jinja) | 很快 | 个人站、零依赖 |
| Eleventy | Node | 多模板语言可选 | 中等 | JS 生态、极简 |
| Astro | Node + Vite | 组件 + islands | 中等 | 内容 + 组件 |
Hugo 和 Zola 是给内容套模板:单二进制、零依赖、构建飞快,代价是模板语言表达力有限,生态(尤其 Zola)偏薄。Eleventy 是JS 版的 Zola:模板语言随便挑、默认零客户端 JS,但构建更慢、也不自带电池。Astro 则是另一条路——用编程语言去构建内容站点。
再往外还有两条分叉支线:Docusaurus / VitePress / Starlight 偏文档,Next.js / Nuxt / SvelteKit 是应用框架(SSG 只是其中一种输出模式),都不在本文的比较范围内。
从 Zola 到 Astro,赢在哪
1. 内容有了 schema,错误在构建时就暴露
Astro 的内容集合用 Zod 定义 schema:字段类型、是否可选、默认值全部写死在 src/content.config.ts 里。文章少写个引号、tags 写成了字符串,构建直接报错,而不是等到页面上渲染出 undefined。Zola 的 TOML 头是”写啥是啥”,校验全靠自觉。
schema: z.object({ title: z.string(), slug: z.string(), date: z.coerce.date(), tags: z.array(z.string()).default([]),})2. 页面逻辑终于是一门真正的语言
标签聚合、按年归档、分页、取”最近用过的 30 个标签”——在 Zola 里要靠 Tera 的 filter/group_by 拼,在 Astro 里就是普通 TypeScript:getCollection() 拿数组,sort、Map、slice 随便用,逻辑还能抽成 src/utils/*.ts 复用。
3. 组件化,以及能直接借用的生态
页头、侧边栏、文章列表都是可复用的 .astro 组件;代码高亮、RSS、Sitemap 各装一个官方或社区集成即可。需要交互时再用 islands 局部水合,其余部分保持零 JS——这是模板型 SSG 给不了的。
4. 心智模型:从”挑一个主题”到”拥有一个前端项目”
Zola 有一等公民的 theme:config.toml 里 theme = "...",theme 通常是放在 themes/ 的 git submodule,本质是另一个提供 templates / static / sass 的 Zola 项目,你能用同名文件覆盖它。Astro 没有这套”换装”机制——它是一个你完全拥有的前端项目;社区所谓的 “themes” 其实是整套 starter,clone 下来就是你的代码。好处是彻底掌控、能直接接组件与 npm 生态;代价是升级不再”一键换主题”。
迁移手记
代码高亮与语言支持
Zola 的高亮内置、开箱即用(syntect),主题是 config.toml 里的 highlight_theme(tmTheme)。Astro 默认用 Shiki(VS Code 主题);如果默认的不够用,可以再叠加 Expressive Code,实现浅色/深色双主题(如 github-light / github-dark 跟随 [data-theme])、长行自动换行、行标记等需求。
语言支持上不必担心:Shiki 基于 TextMate 语法,覆盖面很广。像 bash、yaml、sql、go、powershell、nginx、kusto、regexp、dotenv、csv、cs 这类常见(以及不少冷门)语言都能直接高亮;真遇到未内置的,再往 Shiki 的 langs 里补即可。
中文标题锚点
Zola 会把中文标题转成拼音锚点,迁移时这个行为不能丢。做法是写一个 rehype 插件,用 unidecode + github-slugger 生成 id,并让它在 Astro 自己的标题 id 收集之前执行——这样目录(TOC)和实际锚点自动保持一致:安装 uv → #an-zhuang-uv。
中文标签 → 拼音 slug
同理,标签和分类也要能变成 URL 安全的路径。做法是抽一个 slug 工具函数,用 unidecode + github-slugger 把中文转成拼音,于是 /tags/画图/ 变成 /tags/hua-tu/;若想保持旧链接不变,可以让它对齐 Zola 原本生成的 slug。
接入 Tailwind
用官方 Vite 插件即可:在 astro.config.mjs 的 vite.plugins 里挂 @tailwindcss/vite,样式入口写一行 @import 'tailwindcss'。深色模式不依赖系统偏好,而是绑定自己控制的 [data-theme]:
@import 'tailwindcss';@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));站内链接
Zola 的 [文字](@/2024/foo.md) 会在构建时校验目标是否存在,且移动文件不改链接。迁到 Astro 后一般改成朴素的绝对路径 [文字](/slug/)——更符合直觉、也更好移植,但代价是失去了构建期的链接校验,需要的话得另加 astro check 或 link checker 补回。
代价
换到 Astro 不是没有成本,得诚实说:
- 不再是单二进制。Zola / Hugo 一个可执行文件就能构建,Astro 需要 Node 工具链和
node_modules。 - 构建更慢。这点上 Go / Rust 的 SSG 有天然优势。
- 依赖会漂移。有
package.json就有升级、锁版本、偶尔踩坑的日常。 - 供应链与合规成本,企业里尤其重要。npm 依赖树庞大、CVE 频发;而且构建期恰恰是 CI 密钥所在之处——Shai-Hulud 这类攻击正是靠投毒包的 install 脚本窃取云与代码仓库凭证,所以”只在构建时运行”并不等于安全。更麻烦的是,审计与合规(如 SOC 2 / ISO 27001)通常要求 SBOM 里不存在已知高危 CVE,无论它能否在运行时被利用。相比之下,Zola / Hugo 是单二进制,应用级依赖图几乎为空,维护面小得多。
所以这不是”Astro 全面碾压”,而是需求匹配:只想把 Markdown 铺成一个极简、构建飞快的站点,Zola、Hugo、Eleventy 依然是极佳选择;但当站点开始长出逻辑、组件和交互,Astro 那套”内容 + 组件 + 类型系统”的组合就明显更趁手。
小结
2026 年,静态站点这件事本身没变,变的是我们对它的期待:它不再只是渲染 Markdown,而是一个可以编程、可以复用、可以校验的工程。这次从 Zola 到 Astro 的迁移,真正买到的不是”更炫的框架”,而是把一篇篇内容重新纳入了软件工程该有的约束与实践里。