2026年10月14日
把博客从 Hexo 搬到 Astro 的记录
旧博客只留下了编译好的 HTML,源码已经丢了。这次迁移要同时做到三件事:新站接管根路径、旧链接一条不断、历史可回滚。
这个站原来是一个 Hexo + Stellar 主题的博客。问题不在于它不好,而在于它只能当博客用 —— 首页是文章列表,没有一个地方能说明「我是谁、做过什么」。
我想让根路径变成作品集,同时又不想把以前写的东西弄丢。
真正的约束:根路径只有一个
GitHub Pages 上一个账号只有一个「根」,就是 <username>.github.io 这个仓库。谁占着它,谁就是门面。
于是有三个选择:
- 买域名,把博客拆到
blog.子域 —— 最干净,但要花钱且要配 DNS - 新站占根路径,旧博客整体挪走 —— 不花钱,但旧链接有断的风险
- 新建一个仓库走
/me/子路径 —— 最安全,但门面 URL 就废了
选了第 2 条。关键在于:旧链接会不会断。
关键发现:Hexo 的产物全是绝对路径
动手前我先确认了旧产物里的链接形式,结果都是根路径绝对链接:
<link rel="stylesheet" href="/css/main.css">
<a href="/2025/08/17/how-to-become-fullstacker/">
<a href="/wiki/coding-study/">
这意味着只要保持每个文件的路径不变,内部链接就全都不用改。所以我没有重写任何 HTML,只是把整个目录结构平移到 public/ 下 —— 因为 Astro 会把 public/ 原样拷进产物根目录。
唯一需要特殊处理的是旧站的首页 index.html:它的路径会被新站的首页占用。这个文件的内容在 /archives/ 里已经有了,所以直接删掉,它的内容仍然躺在 git 历史里。
用 git mv,不用 force push
迁移用的是 git mv 而不是「删掉旧的、提交新的」:
111 files renamed(git 把每个文件都识别成了改名,而不是删除+新增)
好处有两个。一是 diff 可读,review 时能看出「这个文件只是换了个位置」。二是历史连续 —— 任何一个文件都能追溯到它在 Hexo 时代的提交记录。
同时我也没有 force push。新站是一次普通的追加提交,旧的 10 个 commit 完整留在 main 上,想退回去直接 git reset --hard 就行。
怎么确认「一条链接都没断」
「我觉得没断」不算数,得有个基线可比。做法是:用 git archive 把迁移前的仓库导出一份到临时目录,然后对迁移前和迁移后分别跑同一套内链检查脚本 —— 解析所有 HTML,抽出根路径绝对链接,逐个映射回文件系统看是否存在。
结果:
| 迁移前 | 迁移后 | |
|---|---|---|
| HTML 数 | 31 | 41 |
| 内部链接 | 948 | 1027 |
| 断链(去重) | 5 | 5 |
| 新增断链 | — | 0 |
那 5 条两版都在,说明是旧博客本来就有的死链,不是迁移造成的。这个「迁移前后对比」比单看迁移后的数字有意义得多 —— 否则看到 5 条断链,你根本判断不出是谁的锅。
顺手修掉的两个旧缺陷
检查过程中暴露了两处旧问题:
邮箱链接缺 mailto: —— 30 个页面都有,写成了 href="/archibaldwan@163.com"。这是 Hexo 配置里把邮箱当成了裸字符串。点上去只会跳到一个不存在的路径。
7 个页面里的图片路径用了 Windows 反斜杠:
<img data-src="\imgs\backup-2023\wallpaper-coder.png">
这个是配置里混进了 Windows 路径分隔符。有意思的是它并没有真的把图弄坏 —— 浏览器解析 URL 时会把 \ 归一化成 /,所以图一直是正常的。我把路径改成了正斜杠,但这属于代码卫生,不是线上故障。
顺便说一句,这里我自己先犯了个错:检查这些图是否存在时,我用 replace('\\', '/') 处理路径,结果开头的反斜杠把路径变成了绝对路径根,/imgs/... 当然找不到,于是我得出了「这些图根本不存在、线上是坏的」的错误结论。后来用线上真实 URL 验证才纠正过来。检查脚本本身也会有 bug,而它给出的错误结论看起来和真结论一样可信。
迁移当时的目录结构
src/ 新站(Astro)
content/projects/ 作品集
content/posts/ 博客文章
content/pages/ 关于页
public/ 旧博客产物,路径原样保留
archives/ 2025/ wiki/ ...
.nojekyll 防止 Jekyll 吞掉 _astro/ 目录
部署走 GitHub Actions:main 每次推送 → 构建 → 发布到 Pages。旧的 10 个 commit 依然在,a6a84a3 就是完整的旧博客。
一点体会
迁移类工作最大的风险不是「改不动」,而是改完之后说不清哪些是原来就坏的。所以先建基线、再动手,比事后解释便宜得多。这次如果没有那份迁移前的对照数据,那 5 条断链就够我排查半天。
补记:后来还是把这些旧内容退休了
上面这套「原样保留旧路径」的方案跑通之后,我又做了一个决定:不再留旧博客的任何东西。
理由是成本一直在、收益是零。43MB 的壁纸和旧主题资源躺在仓库里拖着克隆速度;旧页面要靠 jsdelivr 的 CDN 才能正常显示,CDN 一断就是一片破图;而它本身还带着 6 条从来没修好的死链 —— /notes/java_script、/wiki/archie-life/matters_of_fitness、/bookmark/ 这些,指向的页面早在 Hexo 时代就不存在了。
所以最后清掉了 public/ 下全部 Hexo 产物,一共 109 个文件,只留下一个 favicon.png —— 因为新站的 <link rel="icon"> 还在用它。
代价得说清楚:下面这些 URL 现在全部 404。
/2024/01/01/hello-world//2025/07/30/common-ai-words//2025/08/17/how-to-become-fullstacker//archives/、/categories/、/tags/、/wiki/、/notes/、/notebooks/、/more/
如果它们被搜索引擎收录过、或者被别人链接过,那些链接就断了。我判断这个代价可以接受:那几个页面本来就是早期的学习笔记,不是我想让人看到的东西。
内容本身没有丢。 删之前我把「含全部 Hexo 内容的最后一个提交」打了标签 hexo-final 并推到远端,随时能取回:
git fetch --tags
git show hexo-final:public/2025/08/17/how-to-become-fullstacker/index.html
git checkout hexo-final -- public/2025
这大概是整件事里最值得记的一点:破坏性操作之前先让它可恢复,比事后解释便宜得多。