跳到内容
观澜涉川 · 札记EN

在 2026 年把静态站点从 Zola 迁移到 Astro

· 约 5 分钟阅读

2026 年做静态站点,工具大致收敛成了几条清晰的路线。下面先横向看一圈,再聊聊把一次 Zola 站点迁移到 Astro 之后,实际赢下了什么,以及过程中那些具体的坑——代码高亮与语言支持、中文标题锚点、中文标签转拼音,以及 Tailwind 的接入。

横向看:2026 年的四种形态

Hugo、Zola、Eleventy、Astro 基本覆盖了内容站的四种形态:

工具运行时内容 / 模板模型构建速度定位
HugoGo 单二进制Go 模板极快大站、主题生态
ZolaRust 单二进制Tera 模板(类 Jinja)很快个人站、零依赖
EleventyNode多模板语言可选中等JS 生态、极简
AstroNode + 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 的迁移,真正买到的不是”更炫的框架”,而是把一篇篇内容重新纳入了软件工程该有的约束与实践里。