2020 年我写过一篇《Hexo-backup备份恢复》,那是电脑崩盘后的复盘;这篇是四年停更之后,让站点重新活过来的记录。
开场:四年后打开,最先坏的不是内容
2022 年 9 月,我停更了。2026 年 10 月重新打开这个博客时,我以为要面对的是一堆过时的技术文章。
结果第一件发现的事是:站点配置里的域名指向一个已经停运的服务,站内 199 个页面全部带着死链。
文章过时只是显得旧,死链是实打实的坏——搜索引擎抓到的每个 canonical、每篇 RSS 条目、每个分享卡片里的地址,都指向一个打不开的地方。而且它不报错、不警告,安安静静地把坏链接发给所有人。
先把零件数清楚:一个博客其实是四个仓库
在动手修之前,我先把自己这套系统的”零件”列了出来。这一步很关键——你要判断的每一件事,其实都挂在某个仓库或某个第三方服务上:
| 仓库 / 服务 | 角色 | 停更后会怎样 |
|---|---|---|
| 源码仓库(私有) | 文章与配置 | 只要它在,内容就还在——这是唯一必须保住的东西 |
| 产物仓库(公开) | 构建出的静态文件,同时是 Pages 发布源 | 会”冻”在最后一次构建,看不出坏,直到有人点进死链 |
| 图床仓库(公开) | 全部配图,经 jsDelivr 引用 | 一旦删了就全站图裂;好在它不依赖博客本身 |
| 个人主页仓库 | GitHub 个人页的 README | 和博客无关,但它的自动提交容易被误认成”博客又更新了” |
看清这张表会省很多力气:真正要管的只有源码仓库、产物仓库,以及几个第三方服务,其余的不用碰。
一、体检:静态博客到底依赖多少个外部东西
停更的博客不会立刻死,它会慢慢腐坏。腐坏的位置可以按”谁的锅”分成三类,我先把它们列成一张清单:
| 依赖 | 四年后的状态 | 怎么发现的 |
|---|---|---|
站点域名 / url |
❌ 指向已停运的 gitee.io 子域 | 产物里 199 个页面含死链 |
| 部署方式 | ❌ gitee 部署失效;换 GitHub 后用的 30 天 PAT 也已过期 | 发布失败 |
| 评论后端 | ❌ LeanCloud 免费环境失效,六种评论系统全关 | 文章页评论区是空的 |
| 统计服务 | ⚠️ 开关是关的,但服务端计数还在 | 打开开关后 10601 PV 立刻回来 |
| 图床(jsDelivr) | ✅ 仓库还在、CDN 还能用 | 随机抽图 curl 全部 200 |
| 主题与构建 | ✅ 2022 年的 vendored 主题 + Hexo 6.2,Node 22 直接跑通 | hexo generate 通过 |
这张表里最关键的分层是:
- 产物里的坏(域名):最危险,因为不报错,只是持续把死链发出去
- 链路里的坏(部署凭证):会直接让你没法更新,一 push 就失败
- 别人家的坏(评论、统计、CDN):你控制不了,只能检测和替换
还有一个必须接受的前提:停更的四年里世界变了,而这些变化不在你的仓库里。
- 我当年用的 gitee pages 部署,服务已经停运——产物还能打开,但发布通道没了
- 评论后端用的 LeanCloud 免费环境,免费版已经不再提供,账号里的环境也跟着失效
- 好在图床依赖的 jsDelivr 还在——这条属于运气,不是必然
所以”恢复”真正的难点从来不是把旧代码跑起来(Hexo 6.2 在 Node 22 上一点就通),而是你依赖的那些别人的服务,有几个已经不在原地了。
二、抢救:先连通、再恢复内容、最后清理
第 1 步:修域名,并且做产物级验证
静态站的 url 是编译期常量——它会被写进每一页的 canonical、og:url、sitemap.xml 和 atom.xml。所以改完必须全量重建,而且在产物里验证,而不是只看配置文件:
npx hexo clean && npx hexo generate
# 在产物里搜旧域名,计数必须是 0
grep -rl '旧域名' public/ | wc -l
这一步做完,199 个死链才算真的消失。教训:域名类的配置改完,验证要看产物,不看源码。
第 2 步:把发布链路换成”不需要我管”的
原来的部署方式依赖 gitee,服务停运后就断了;后来临时用 GitHub 的 PAT,30 天过期又断一次。最终方案是 GitHub Actions 全自动:
push 到源码仓库 main
→ Actions: checkout → setup-node(带 npm 缓存) → npm ci → hexo generate
→ peaceiris/actions-gh-pages 把 public/ 推到产物仓库
→ GitHub Pages 发布,约 1 分钟上线
两个关键参数,都是踩过才知道为什么要:
- 用 deploy key,不用 PAT:deploy key 可以设成永不过期,配一次就不用再管;PAT 到期那天你会收到一个”发布莫名其妙失败”的惊喜
- 开
force_orphan:产物仓库每次部署只保留一个提交(我验证过,master 上永远只有 1 个 commit)。产物不需要历史,而且这样能绕开产物仓库那边的分支保护冲突
实际效果:push 到上线 36~57 秒。
顺带补两个容易被忘的产物:sitemap.xml 和 atom.xml。它们是搜索引擎收录和 RSS 订阅的入口,恢复后要确认它俩真的躺在产物里——我这次就是靠它们回来,站点的收录和订阅才有出口。
顺便算一下成本:Actions 对私有仓库有每月免费分钟数,而这里一次构建只要 1 分钟左右,日常更新根本碰不到上限;Pages 本身免费。整次恢复的现金成本是 0。
第 3 步:一个”不是密钥”引发的发布失败
有个坑值得单独写:评论库的某个文件里带了一个腾讯云 SecretId 格式的示例占位符——它不是真密钥,但 GitHub 的 push protection 只认格式,直接拦下推送,于是发布失败。
修法是把占位符换成低熵字符串,并且在项目文档里写死一句:”不要从官方重新下载这个文件”。这类问题的麻烦之处在于:它拦的不是你的密钥,是一个看起来像密钥的字符串。
第 4 步:从旧产物和备份分支里”考古”
内容之外的东西最容易丢。我这次捞回了两样:
① 备份分支。 产物仓库里有个 2020 年的 backup 分支(提交信息写着 “Back up my www.wulinzeng.vip blog”,815 个文件),里面藏着当前仓库没有的 source/_data/friends.json——13 条友链数据,字段和现在的模板完全对得上。友链页的正文(”欢迎友链 + 换链规则”)也从那里捞了回来。
② 旧部署产物本身就是快照。 项目里残留着 2022 年的产物目录,我拿它对照确认了一件事:2022 年那个友链页本来就是空的。这很重要——它说明”友链功能从来没生效过”,而不是”被我弄丢了”。判断”是丢了还是从没有”会决定你花多少时间去抢救。
顺手做了一次死链体检,13 条友链逐条 curl:
| 结果 | 数量 |
|---|---|
| 正常 | 2 条 |
| Gitee Pages(服务已停运) | 2 条(404) |
| 域名无响应 | 8 条 |
| 服务器报错 | 1 条(503) |
只把活着的 2 条放回页面,并且给其中一条换了头像地址(它原来的 jsDelivr 链接已经失效)。友链页看着冷清,但比挂一堆死链诚实。
第 5 步:统计开关关了,数据居然还在
这个发现挺意外:不蒜子(busuanzi)的开关一直关着,但打开之后四年前的数字立刻回来了——10601 PV、9892 UV。原因是它按域名在服务端计数,前端开关只是在决定”要不要去取”。
顺带一个自测时的坑:它的接口必须带 Referer,不带 Referer 直接返回 400。我一开始用命令行裸测,以为服务已经死了,差点做出错误判断——测试方式错了,会把”活着”误判成”死了”。
三、改名:牵动的地方比想的多
站点名从旧名字改成了现在的 Halensea / 海上生林。实际要改的只有 4 处:_config.yml 里的 title、subtitle、author,加一个独立页面的 HTML 标题。
但更值得记住的是有哪些看着像名字、其实绝对不能改:
- GitHub 用户名:站点
url依赖它,改了等于全站链接作废 - 图床的 CDN 路径:二十多篇文章的配图全指着
cdn.jsdelivr.net/gh/<用户名>/blogimg/...,动一下就是全站图裂
顺手清掉的旧品牌残留(这类东西不清理,会让人以为站点停在上一个时代):
- 页脚的 “Hosted · Gitee” 徽章——站点早就不在 Gitee 了
- 页脚的 “DNS · Tencent” 徽章——它的文案说的是”提供域名服务”,但站点根本没有自定义域名
- 导航里的”开往”外链——指向的旧域名需要换新址,后来干脆移除
四、四个”看起来没问题、一用就坏”的坑
这四个都是这次实测出来的,写出来免得别人再花同样的时间。
坑 1:markdown 会吃掉 LaTeX 的反斜杠
源文件里写 0\\1\\0\\0(LaTeX 矩阵的换行),markdown 会把 \\ 转义成 \,到 HTML 里就成了 0\1\0\0——矩阵渲染成一行字面量。
修法:源文件里写 \\\\(四个反斜杠),markdown 处理后正好剩两个。我在浏览器里做了对照:单反斜杠渲染成 [0\1\0\0],四个反斜杠才是正确的竖排矩阵。
坑 2:加粗标记有隐藏的失效边界
**…** 里的内容如果含 ASCII 标点(比如 %、英文引号),而且关掉的 ** 后面紧跟中文标点,加粗会失败,页面上直接显示两个星号。
我做了十组对照实验才摸清规律,结论很简单:内容里用中文标点、或者给 ** 两侧留个空格、或者干脆用 HTML 的 <strong> 标签,都能绕开。
坑 3:公式引擎的冷缓存竞态(这条我还没修)
一篇讲深度学习的文章,我实测到的结果是:首次访问(冷缓存)时整页 290 个公式全部报错,显示 [Math Processing Error];而刷新一次就全部正常。
查下来:公式引擎的组件文件都能下载到,配置文件也对——问题出在”页面已经开始处理公式了,引擎的组件还没加载完”这个时序上,而那个 CDN 域名现在已经不是一个正常服务的 CDN,加载慢把窗口放大了。修法有三个方向(换 CDN、自托管、升级大版本),我还没定。
写这篇文章时我甚至绕开了公式——两个公式只能塞进代码块里当纯文本。这就是”基础设施坏了”的真实代价:它会改变你写什么。
坑 4:默认配图是哈希选的,不是随机
主题在文章没指定封面时,会按标题的哈希值从固定图池里挑一张默认图。这意味着:
- 同一标题永远得到同一张图(不是随机)
- 改标题会换图
- 增删图池里的图片,会让一批文章的配图整体重排
我把首页八张卡片逐张算过,8/8 全部命中这个规则。想让某篇固定住,就显式指定封面。
五、如果重来:停更前我会做的三件事
- 写一份”给未来的自己看的说明书”放仓库里:域名在哪、怎么部署、凭证怎么配、图床在哪。四年后我最缺的不是代码,是”当年我是怎么想的”
- 凭证一律换成长期有效的(deploy key / SSH),不用短期 token——它一定会在你最不想折腾的那天过期
- 备份范围要包含”看不见的东西”:不只是 markdown,还有
_data数据、图床仓库、评论后端凭证、统计账号。这次最容易丢的恰恰是这些,而它们不在source/_posts/里
六、现在的样子,以及还没修好的
已经好的:push 到上线 36~57 秒全自动;域名和产物干净(旧域名在产物里 0 命中);统计恢复(10601 PV 回来了);友链只留活着的;品牌统一;发布链路不再依赖任何会过期的凭证。
还没修的(诚实列出来):
- 公式渲染方案没定,所以带公式的文章仍有”首次访问全挂”的风险
- 评论后端还是全关状态——导航里那个”留言板”点了是空的
- 三篇旧文有加粗渲染失败(页面上显示字面星号)
- 部分配图 1~2 MB,首屏会等
附:一份可以直接复制的体检脚本
如果你也有一个躺了很久的静态博客,下面几条命令能在一分钟内告诉你”伤在哪”:
# 1) 站点配置里的域名是不是还活着
grep -E '^url:' _config.yml
curl -sI "$(grep -E '^url:' _config.yml | awk '{print $2}')" | head -1
# 2) 产物里有没有残留的旧域名(期望 0)
npx hexo generate && grep -rl '把这里换成旧域名' public/ | wc -l
# 3) 关键文件在不在(搜索引擎和订阅的入口)
ls -lh public/sitemap.xml public/atom.xml
# 4) 抽三张图床图片验证 CDN(期望 200 + image/*)
grep -rhoE 'https://cdn\.jsdelivr\.net/gh/[^)"]+' source/_posts | head -3 | while read u; do
curl -s -o /dev/null -w "%{http_code} %{content_type} ${u:0:60}…\n" "$u"
done
# 5) 发布链路还通不通(看最近一次工作流)
gh run list --limit 1
检查顺序就按这个来:域名 → 产物 → 关键文件 → 外部依赖 → 发布链路。前三个是你的锅(能修),后两个要先检测再决定是换还是留。
一句话总结
停更的博客不会”坏掉”,它会静静地腐坏——而且腐坏的地方,恰恰是你备份不到的地方。
所以真正该做的不是”等有空了再更新”,而是趁站点还活着,把域名写清楚、凭证换成长效的、把那些看不见的依赖也记进文档。四年后你回来时,会感谢现在的自己。