astro-koharu 升级 Astro 6 历程记录

发布于 2026-07-20 03:50 更新于 2026-07-23 20:57 3799 字 19 min read ... 访问量

cos avatar

cos

FE / ACG / 手工 / 深色模式强迫症 / INFP / 兴趣广泛养两只猫的老宅女 / remote

记录 astro-koharu 为 v5.0.0 升到 Astro 6 的过程:保住历史 URL 和旧备份,也为 Live Content Collections 与可选轻后台打地基。

实际迁移过程的复盘,AI 参与了复盘整理。

最近把 astro-koharu 从 Astro 5.16.6 升到了 6.4.8。

一开始以为还是熟悉的三件套:升依赖、改 API、修 build。真正动手后才发现,Astro 本身不算最麻烦,项目背着的历史数据才是: 旧文章里的 sluglink、多语言 fallback、搜索索引,以及可能跨了几个版本的备份。

所以这次的目标很朴素:

  • 页面别少;
  • URL 别变;
  • 旧文章不用手改;
  • 旧备份恢复后还能直接构建。

另一个目标就没那么保守了:给 v5.0.0 战未来。

v5.0.0 准备用 Astro 6 打底,一个很重要的原因就是 Live Content Collections。我不想把 astro-koharu 直接改成必须连后端的 CMS, 而是想保留现在开箱即用的静态构建,再留一条可选的动态路径:不配后台时照常生成静态站;需要时再接一个轻后台,拿到动态内容和发布能力。

理想状态是静态和动态各取所长。再往后,我甚至可以直接把文章发给爱酱,让她帮我整理、写入并发布博客。这个想法现在还没落成, 但如果底层一直停在旧 Content Collections 上,后面每走一步都要先还技术债。

最后不只把模板升到了 Astro 6,也做了一套可 dry-run、自动备份、重复执行不出事的内容迁移。顺手拿 376 篇真实旧文章跑了一遍, 确认不是只对测试有效。

先别急着改版本号

Astro 6 升级指南要求 Node.js 22.12+,同时带来了 Vite 7、Zod 4、 Shiki 4 和 Content Layer 的变化。迁移时 astro@latest 已经进了 7.x,所以这里直接锁定目标版本:

{
  "packageManager": "pnpm@10.28.2",
  "engines": {
    "node": ">=22.12.0"
  },
  "dependencies": {
    "astro": "6.4.8"
  }
}

动代码前先记下 Astro 5 的基线:

指标Astro 5.16.6
astro check347 个文件,0 error / warning / hint
Astro 静态页面130
Pagefind 索引页面134
dist/ 路由产物141
Astro 构建耗时约 21.62 秒

这里最重要的不是构建时间,而是公开行为:

  • 默认语言继续不带前缀,英文和日文仍然使用 /en//ja/
  • frontmatter 的 link 继续控制公开 URL;
  • 未翻译文章的 fallback 和 canonical 不变;
  • GFM、Shiki、KaTeX、Mermaid、Pagefind、RSS、sitemap、robots 都要正常生成。

这轮先不把 Fonts API、CSP 和轻后台一起塞进迁移。Live Content Collections 不是“不适合”,恰恰是升级 Astro 6 的理由之一; 只是要先把内容身份、历史数据和迁移路径收稳,再往上接动态能力,不然出了问题很难判断是框架迁移还是新功能。

依赖不用全部追最新

核心依赖最后落在这些版本:

依赖迁移前迁移后
astro5.16.66.4.8
@astrojs/react4.4.25.0.7
@astrojs/rss4.0.144.0.19
@astrojs/sitemap3.7.03.7.3
shiki3.22.04.3.1
astro-mermaid1.2.02.1.0
astro-pagefind1.8.51.8.6

这里忍住了把 astro-pagefind 升到 2.x 的冲动。2.x 支持 Astro 6,但搜索组件也换成了 Web Component API,继续升就得连搜索样式、 键盘操作和 i18n 一起重写。

1.8.6 已经补了 Astro 6 的 peer 支持,又保留旧 UI。对这次迁移来说,它比“最新版本”更合适。

真正的改动:内容 ID 不再等于公开 slug

旧项目使用 src/content/config.tspost.slugpost.render()。Astro 6 改成了显式 loader,配置移到 src/content.config.ts,Zod 也从 astro/zod 导入:

import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blogCollection = defineCollection({
  loader: glob({
    pattern: '**/[^_]*.{md,mdx}',
    base: './src/content/blog',
  }),
  schema: z.object({
    title: z.string(),
    // 其他 frontmatter...
  }),
});

API 不难改,容易改错的是内容身份。

在这个项目里,几个名字很像,实际不是一回事:

  • entry.id:Markdown 相对路径,给内部查找用;
  • 公开 slug:经过 locale 处理和字符转写后,出现在 URL 里;
  • frontmatter.link:用户显式指定的永久链接,只覆盖公开 slug。

因此只替换真正属于 Content Collection 的身份读取:

export function getPostLocale(post: BlogPost): string {
  return getSlugLocaleInfo(post.id).locale;
}

export function getPostSlug(post: BlogPost): string {
  return post.data.link ?? transliterateSlug(getSlugLocaleInfo(post.id).localeFreeSlug);
}

路由里传内部 ID,再使用新的顶层 render()

import { render } from 'astro:content';

return posts.map((post) => ({
  params: { slug: getPostSlug(post) },
  props: { postId: post.id },
}));

const post = await getPostById(postId, locale);
if (!post) throw new Error(`Post not found: ${postId}`);

const { Content } = await render(post);

Astro 6 里 body 的类型允许 undefined,摘要提取也要顺手兜一下:

extractTextFromMarkdown(post.body ?? '', maxLength);

不能全仓把 slug 替换成 id。搜索结果、相似文章和系列导航里也有 slug,它们表达的是公开路径。机械替换能过类型检查, 却可能悄悄改掉线上 URL,这种 bug 比直接 build 失败更烦。

376 篇文章,不可能让用户手改

旧文章里最棘手的是顶层 slug。它以前承担过永久链接语义,Astro 6 的 loader 又开始用文件路径生成 entry ID。 直接删会丢 URL,原样保留则继续混淆两套身份。

所以 Koharu CLI 多了一条正式迁移命令:

# 先看计划,不写文件
pnpm koharu migrate --dry-run

# 自动备份后再执行
pnpm koharu migrate

迁移规则尽量少动文章:

文章现状处理方式
只有 link不改
同时有 linkslug保留 link,删除冗余 slug
只有 slug原位改名为 link
两者都没有使用路径;译文优先跟随默认语言的稳定链接
出现重复 URL 或多个候选链接整批停止,交给用户决定

实现时给它加了几条底线:

  • 扫描所有 .md.mdx,不假设目录结构永远整齐;
  • 只编辑目标字段,不重新序列化整段 YAML;
  • 解析错误、空链接、重复 URL 和符号链接都会在写入前阻止整批迁移;
  • 写入前再次检查文件和配置有没有变化,避免拿过期计划覆盖用户刚改的文章;
  • 单文件使用临时文件加原子 rename,后续失败则逆序回滚;
  • 正式执行前自动备份,再跑一次 dry-run 必须是 0 项变更。

多语言文章也不能只看文件名。默认语言文章如果把 note/old-name 改成了 my-stable-url,英文译文就应该跟着这个公开链接, 而不是重新从自己的路径算一个。反过来,默认语言没有显式 link 时,路径就是已经存在的 URL,也不能被译文反向覆盖。

pnpm devpnpm build 现在都会先跑 pnpm koharu migrate --check。它只检查,不会擅自改文章;发现旧字段或冲突时, 直接在 Astro 启动前给出迁移提示。

还有一个进程边界很容易忽略:旧版的 pnpm koharu update 运行时,内存里还是旧 CLI。必须等它完全退出,再启动新命令:

pnpm koharu update

pnpm koharu migrate --dry-run
pnpm koharu migrate

不要指望正在运行的旧进程突然学会更新后才出现的 migrate 子命令。

旧备份也不能继续 cp -r

内容迁完后又发现一个老问题:旧恢复逻辑会把备份目录直接合并进新目录。

假设新版模板多了一篇示例文章,而旧备份里没有它。普通复制不会删除这篇文章,恢复完成后,用户站点就会凭空多出模板内容。 文件系统觉得自己成功了,用户不会。

所以恢复要区分快照和配置:

恢复对象恢复方式
src/content/blog清空目标后恢复,必须与备份快照一致
public/img同样按快照替换,避免已删除图片残留
src/pages/*.md替换独立 Markdown 页面,保留新版 .astro 路由
config/合并恢复,既还原用户配置,也接住新版配置项
summaries、similarities、LQIP只跟随完整备份恢复

新备份 manifest 使用 schemaVersion: 2,没有版本字段的旧归档仍按 v1 读取。恢复前会检查 manifest、归档内容、文件类型和符号链接; 解压后也不会立刻覆盖,而是先准备候选快照、跑内容迁移,全部通过再切换。中途失败就回滚。

基础备份不包含摘要、相似文章和 LQIP,恢复后需要重新生成:

pnpm koharu generate all

完整备份才会带上这些与文章快照配套的派生数据。

Build 里依次炸出的四个坑

1. Markdown 配置换入口了

旧配置把 gfmremarkPluginsrehypePlugins 直接放在 markdown 下,Astro 6.4 会给弃用提示。 这个项目的 Markdown 管线又很长,直接删肯定不行,最后改成显式 processor:

import { unified } from '@astrojs/markdown-remark';

markdown: {
  processor: unified({
    gfm: true,
    remarkPlugins,
    rehypePlugins,
  }),
  syntaxHighlight: {
    type: 'shiki',
    excludeLangs: ['mermaid'],
  },
}

Shoka 语法、链接嵌入、Mermaid、KaTeX 和 Shiki 都继续走原来的处理顺序。

2. react-tweet 的 CSS 没进 Vite

第一次生产构建直接报:

Unknown file extension ".css"
Hint: add the package to vite.resolve.noExternal

旧的 vite.ssr.noExternal 没覆盖 Astro 6 的预渲染 environment。按错误提示挪到这里:

vite: {
  resolve: {
    noExternal: ['react-tweet'],
  },
}

3. RSS 撞上 Zod 4

@astrojs/rss 4.0.14 会报:

z.function(...).returns is not a function

不是 feed 数据坏了,而是旧版本还在调用 Zod 4 已移除的 API。升级到 4.0.19 后,默认、英文和日文三份 RSS 都恢复了。

4. i18n 配置保留了一个不存在的行为

原配置同时使用 prefixDefaultLocale: falseredirectToDefaultLocale: true,Astro 6 不再接受。

先回头查 Astro 5 基线,发现 /zh//zh/post/* 本来就是 404,根本没有重定向需要兼容。于是删掉无效配置, 继续保留 /rss.xml/en/rss.xml/ja/rss.xml 这三条正式 feed URL。

这也是为什么迁移前要留基线:有些“兼容配置”只是看起来像在工作。

怎么确认站点没被悄悄改坏

先比较加入本文之前的模板产物:

指标Astro 5Astro 6
静态页面130130
Pagefind 索引页面134134
dist/ 路由产物141141
Astro 构建耗时21.62s21.76s

数字对上只是第一层,还检查了:

  • //en//ja/ 和代表性文章返回 200,/zh/ 继续 404;
  • fallback 页面仍显示默认语言正文,canonical 仍指向默认语言 URL;
  • 生成 HTML 里能找到 Pagefind body、Shiki、KaTeX 和 Mermaid 标记;
  • Pagefind 实际查询可用,RSS、sitemap 和 robots 正常生成;
  • pnpm 10.28.2 可以用 frozen lockfile 干净安装并完成生产构建。

迁移脚本另外有 42 个测试,专门覆盖重复链接、危险 frontmatter、符号链接、并发编辑、恶意归档、写入失败回滚,以及把旧归档 恢复到带有新版模板内容的目录。测的是数据边界,不是 CLI 文案。

最后再拿一份长期使用的真实仓库做恢复:

检查项结果
历史 Markdown / MDX376 篇
已有稳定 link375 篇
需要自动补齐1 篇
同语言 URL 冲突0
恢复后的内容数量376 篇
astro check0 error / warning / hint
Astro 6 静态页面797
Pagefind 索引页面801

这个结果比 fixture 更有说服力:新版模板示例没有残留,375 篇文章不用重写,只有 1 篇需要补 link。旧文章里原有的 KaTeX、 Shiki 语言别名和外链抓取 warning 仍然存在,但没有阻止类型检查和 797 个页面的生产构建。

先守住静态,再接轻后台

当前版本仍然默认走 static + nginx,但这是默认部署方式,不是未来架构的上限。

我想要的轻后台必须是选配:

  • 不启用后台时,博客继续完整静态构建,部署方式和现在一样;
  • 启用后,再通过 Live Content Collections、按需渲染或缓存能力接动态内容;
  • 发布入口也不必绑死在传统 CMS,后面可以接 Bot、AI 助手或其他自动化流程。

v5.0.0 本身会是一轮破坏性升级,但“有破坏性变更”不等于“把迁移成本丢给用户”。内容结构和内部 API 可以重做,已有文章、 永久链接和备份必须尽量自动过桥。迁移脚本也不是写完几组 fixture 就算结束,至少要先拿自己的 376 篇历史文章完整跑通, 确认恢复、迁移和生产构建都没问题,才敢发正式版本。

其他新能力还是拆开处理:

  • Fonts API 值得单独研究,但现有博客已经有约 20 MB 的中日文字体分片和按 locale 加载;
  • CSP 会碰到 ClientRouter、Shiki、inline script 和第三方资源,应该单独做;
  • Sonda 继续通过 ANALYZE=true pnpm build 按需开启。

Rust compiler 和 queued rendering 看起来只要一行配置,还是同机各跑了一次:

模式real timeAstro time最大 RSS
默认25.29s22.89s2.30 GB
queued rendering26.60s24.48s2.36 GB
Rust compiler 0.3.126.24s23.66s2.47 GB

单次结果不算严谨 benchmark,但已经足够回答“要不要留在主干”:这套项目上没看到收益,于是配置和依赖都删了。

最后整理出的迁移顺序

如果项目也有历史内容和多语言路由,我会按这个顺序再做一次:

  1. 记下 Node、check、build、路由集合和代表性 HTML 基线;
  2. 锁定目标 major 和包管理器版本,不直接追 latest
  3. 升 Astro 与官方 integrations,先看第一批真实错误;
  4. 单独处理 content config、entry.id、公开 slug 和 render(entry)
  5. 对历史文章先 dry-run,有冲突就整批停;
  6. 正式迁移放在自动备份后,再跑一次 dry-run 验证幂等;
  7. 分清备份里的快照目录与合并配置,恢复后跑同一套迁移;
  8. 最后比较路由、HTML、Pagefind、RSS,并用一份真实旧备份做完整构建。

astro-koharu 用户实际只需要记住这几行:

pnpm koharu update

pnpm koharu migrate --dry-run
pnpm koharu migrate

pnpm check
pnpm build

先等 update 完全退出,再运行后面的新版 CLI。已有 link 会原样保留,旧 slug 会转成 link,忘了迁移时 dev/build 的只读检查 也会先拦下来,不会静默改文章。

最后

这次升级真正理清的是三件事:entry.id 管内部查找,公开 slug 管 URL,link 只在用户明确指定时覆盖后者。文章和图片是用户快照, 配置则需要在恢复用户值的同时接住新版能力。

版本号从 5 变成 6 不难,pnpm build 变绿也不算结束。等一份有 376 篇历史文章的旧备份能无损恢复、URL 不漂、生产构建完成, 这次迁移才算真的跑通。

Astro 6 也不是终点。它只是先把静态博客的地基换稳,再给 Live Content Collections、可选轻后台和爱酱代发博客留一扇门: 平时继续安静地生成静态页面,想折腾时也不会被纯静态架构锁死。

喜欢的话,留下你的评论吧~

... 访问量
© 2020 - 2026 cos @cosine
Powered by theme astro-koharu · Inspired by Shoka