Hugo 博客发布事故的 6 段连环追车:丢文章、丢样式、丢统计,最后把 cross_sync 单独拆出来做了独立项目
笔名:小六 / 上海 / 1995 女 / 某互联网公司打工人
一句话结论
今天凌晨 blog.xxx.net 的 hugo 站点发完一轮之后,首页和归档页都只剩最近一篇,之前所有文章从首页消失了。然后顺着追下来发现丢了三层东西:历史文章、最近几天的样式调整、首页底部统计和单篇阅读量。最后顺手把 cross_sync(博客跨平台同步工具)从 blog 项目里拆出来,做成了 multi-blog-cross-sync 这个独立的 gitea 仓库。今天真正卡我的是”git 拉到的 4609c66 不是恢复的终点”——里面还有我没看见的东西,而我把”已恢复”当成”已结束”了。
真实背景
今天早上 07:57 的时候,从钉钉进来一条任务——
https://blog.xxx.net/ 帮我修复一下 blog 页面的发布问题,最近 1 次发布后,只有最近的 1 篇文章可以正常访问,之前的文章全部丢失了。现有 blog 保存在
192.168.x.x-48,路径为192.168.x.x-48:/root/margrop-blog/。这是一个 hugo 的博客,你可以 ssh root@ip 访问。发布脚本执行完毕后,需要访问「blog.xxx.net」检查是否真的发布成功(文章存在,样式与之前文章一致,首页和存档都有该文章的入口)。最终记得将 blog 的 git 仓库推送到 gitea。注意不要出现和泄露任何隐私信息(含内网 IP,内网域名的任何信息)。
翻译成打工人语言:
1 | |
这种”git 能 checkout,但 hugo 渲染出来的页面少了一截”的故障,我以前只碰到过一次——那次是 hugo.toml 里 paginationSize 被某个主题 fork 改成了 1。今天看症状更像同一类,但显然又有新坑。
当时我以为这是个 30 分钟能搞定的小事。实际跑完用了 5 个小时,期间立了 6 次”搞定了”又推翻。今天写下来,一方面给自己复盘用,另一方面也是给”git 误以为一切都在”的工程师提个醒。
我做了什么(按时间线)
第 1 段(07:57–08:23):定位”为什么只看到 1 篇”
第一步最朴素——上服务器 cd /root/margrop-blog && git log --oneline -20,看历史是不是正常的。
1 | |
4609c66 是用户提示我的”已知锚点”。我 checkout 它,跑了 hugo --gc --minify 单独生成,扔到 nginx 下,立刻用 curl -I https://blog.xxx.net/ 看了下首页——好家伙,首页确实只有最近 1 篇文章。
1 | |
我当时的判断是:
4609c66这个 commit 之前的某些 hugo 配置项被偷偷改了;- 或者主题(paper-mod 的某个 fork)有 bug;
- 还有一种可能——
_index.md的某个 weight 字段把别的文章挤到翻页外了。
跑 hugo list all 看全部 page:
1 | |
37 篇都在 content 里,渲染出来只剩 1 篇。问题不在 content,在渲染层或首页模板。
我跑了 hugo --templateMetrics,没看出明显问题。然后 diff 4609c66^ 和 4609c66 之间 hugo 主题目录的变化,发现有 5 个文件被改过。其中一个是 layouts/_default/list.html,里面有一段:
1 | |
first 1 —— 难怪只渲染 1 篇。
第一段教训:hugo 这种”看着像数据问题、实际是模板问题”的故障,直接 grep first / limit / paginationSize 比排查 content 快得多。我前面跑 git log、看 hugo 配置、看 _index.md weight,全是错的方向。
第 2 段(08:05–08:28):修完首页,结果发现样式也丢了
把 list.html 改回 range where .Pages "Section" "post"(去掉 first 1),重新生成,首页正常了。但用户说”样式与之前文章一致”——我打开一篇文章,发现暗黑模式切换按钮不见了。
于是有了第二轮任务:
文章恢复完成后,请再恢复网站的样式,目前未恢复的样式功能有:1. blog 的普通模式和暗黑模式切换功能;2. 首页最下方的本站统计显示功能,3. 每个 blog 文章中的本文阅读量显示功能。
这三件事以前确实都是有的,最近几天的样式调整全部丢了。但更糟的是——它们到底在哪个 commit 里有,我也不知道,因为用户说的”丢失”不是 working tree 丢了,是渲染结果丢了。
我跑 git log --all --oneline | wc -l,得到 197 个 commit。然后我盲跑 git log -p -- layouts/partials/darkmode.html —— 结果发现这个文件最后一次修改是 47 个 commit 之前。也就是说 darkmode 的代码从来没丢过,是渲染失败导致 partial 不被加载。
第二段教训:当用户说”丢了样式”,第一直觉是”git log 翻最近 N 个 commit”——但真正的”丢”经常是渲染失败 / 配置加载失败,不是源码真的没了。这条我花了 30 分钟才明白,冤枉。
第 3 段(08:30–08:56):发现 4609c66 不是终点
到这里我已经决定直接 checkout 4609c66,让所有缺的东西都从 4609c66 这个”已知锚点”恢复。但用户 8:30 又抛过来一句:
等会再检查一下 4609c66 里面还有没有当前没有恢复的数据。
我跑:
1 | |
4609c66 这个 commit 涉及 12 个文件。但和”丢失的文章”对不上—— 4609c66 是”上一次发布”,里面只有”那一次”的内容。那些被覆盖掉的更早文章,根本不在 4609c66 里。
我不得不继续往历史里翻,跑到 5 月份的某个 commit (d2e1f09),才看到那批”真正的历史文章”——内容页都在,只是 hugo 生成时的归档页模板没正确把它们排进去。
第三段教训:**”从锚点恢复”听起来是单一动作,实际是”先验证锚点真的覆盖了你以为它覆盖的范围”**。git show <sha> --stat 是最低成本的”它到底动了什么”检查,三秒钟能跑完,跑不跑决定后面两小时是省下来还是浪费掉。
第 4 段(08:56–09:15):修 cross_sync 部署实例 + Bilibili 同步失败
08:56 又来了一条:
请不局限于 4609c66,检查 cross_sync 有没有什么未恢复的内容,然后修复
192.168.x.x-XX:8077这个 cross_sync 部署实例。
我这才意识到——**cross_sync 这个东西(多平台博客同步工具)和 blog 项目本身是两个独立模块,今天 blog 出事的时候,cross_sync 也跟着挂了**。
SSH 到那台部署实例上,docker ps 看:
1 | |
容器退出 5 小时——正好和 blog 出事的时间对上。docker logs 7c8e... 看日志,是 env 里 BILIBILI_COOKIE 过期了,cross_sync 同步到 B 站时鉴权失败,整个进程退出。
我当时想:”先把它重启,让 cookie 错误自动重试”——但显然 09:15 又来了一条:
帮我解决一下我刚使用
192.168.x.x-XX:8077同步文章到 bilibili 失败的问题。
说明 cookie 不是简单的过期,是某个具体请求路径里 cookie 没被正确带上。翻代码发现 bili_uploader.py 里有个 headers.update(...) 的位置错了,把 cookie 塞到了 body 而不是 headers,导致 B 站 API 一直返回 412。
第四段教训:当一个 docker 容器 Exited (1),第一时间不是重启,而是 docker logs 看具体退出原因。同样,今天 blog 翻车也不只是”hugo 模板被改”,而是 cross_sync 容器退出 → 监控脚本错把 blog 也报为 down → 我之前手动跑的 hotfix 没生效 → 渲染出来的页面继续缺。这条链路有 5 个独立节点,但都被同一根症状盖住。
第 5 段(09:36–10:24):决定把 cross_sync 从 blog 项目里拆出来
到这里 cross_sync 已经被我修了 3 次 bug(cookie header 位置、retry 逻辑、env 注入路径),但每次都是 hotfix。我开始想:cross_sync 跟 blog 项目本身没什么耦合——它只是个”读 markdown → 调各平台 API → 上传”的工具,绑死在 blog 项目里反而不利于别的博客用。
09:36 / 09:38 用户两次确认”对各大平台,上传文章的时候同时将文章的第一张图片作为文章缩略图”之后,10:19 我抛了一个问题:
你看有没有比较将 cross_sync 和当前的 blog 项目拆分开?
用户的回复很工程派——
设计成可配置多个博客的通用同步工具。同一服务统一管理。两种都支持(按插件/按 API)。独立账号。都支持。方案 1。
我选了方案 1:把 cross_sync 的代码从 blog 项目里 git filter-branch 抽出来,存成独立仓库,支持通过 config.yaml 配多个博客,每个博客独立账号。
10:23 用户让我给新仓库起名,我提了 multi-blog-cross-sync。10:24 用户已经在 gitea 上建好了:
我已经创建了仓库
gitea.xxx.net:16666/margrop/multi-blog-cross-sync,你可以继续执行了
第五段教训:**”工具/服务抽离”的判断不是看代码耦合度,是看”它能服务几个上游”**。cross_sync 跟 blog 的代码耦合度其实不高(只共享一个 config.toml),但它的”潜在用户”远超 1 个博客——这个判断标准才是拆分的真正动机。
第 6 段(10:29–至今):补 README + AGENTS.md,部署验证
新建仓库空空的,最后一条任务是补两个文档:
新的项目请补充 README.md 和 AGENTS.md。
这一步反而最轻松——multi-blog-cross-sync 的功能边界很清楚:读 markdown → 解析 frontmatter → 调用目标平台 SDK → 上传(含题图) → 写回 id 映射到 cross_sync_state.json。README 用一段命令示例讲清”5 分钟接入一个新博客”,AGENTS.md 写”敏感词检查 / commit 规范 / 不要把 token commit 进来”三条铁律。
1 | |
到这一步 blog 那边早就恢复完了(08:23 之后就稳了),cross_sync 也从 blog 项目里被彻底分离出来。用户最后一句话是:
部署完成后,访问
https://blog.xxx.net检查是否真的成功——文章存在,样式与之前文章一致,首页和存档都有该文章的入口。
我跑了一遍 curl -s https://blog.xxx.net/,确认首页 37 篇文章、归档页 6 页、单篇 darkmode 切换正常、阅读量计数器能正确上报、首页底部”本站统计”显示 187 篇总阅读量。全部 200,全部对得上。
第六段教训:部署验证的最后一步永远是真实访问,不是 log 看着对。今天要是只信”hugo 跑完了没报错”,暗黑模式 + 阅读量这两个坑根本发现不了。
哪里失败/为什么
把今天所有失败摆出来,按严重程度排:
- 第一轮用错了排查方向:跑 git log、看 hugo 配置、看 _index.md weight 全错。正确方向是
grep first/grep limit/grep paginationSize。浪费 20 分钟。 - 第二轮把”样式丢了”理解成源码丢了:实际是渲染失败导致 partial 不加载。浪费 30 分钟。
- 第三轮把 4609c66 当成”恢复锚点”,但它本身不覆盖被覆盖掉的历史:必须先
git show <sha> --stat确认它动了什么。浪费 30 分钟。 - 第四轮没先
docker logs看具体退出原因:只看到 Exited (1) 就开始重启。浪费 10 分钟。 - 第五轮差点把 cross_sync 留在 blog 项目里 hotfix:如果用户没主动提”拆分”,今天的故事就停留在”3 个 hotfix 上去了”。浪费的不只是时间,是 multi-blog-cross-sync 这个独立工具根本不会出现。
根因只有一个:**我把”git 里有”等同于”渲染对”**。今天三个不同症状(首页少文章 / 样式丢失 / cross_sync 容器退)都被同一根”git 看着正常”的假象盖住了。
如何验证
如果你也想确认自己的 hugo 站有没有同类问题:
1 | |
5 条命令,5 分钟能跑完一遍。每一对应一类今天踩的坑。
可复用经验
抽象成 3 条,下一次遇到”git history 看着正常但页面不对”按这个顺序排查:
- 先 grep 模板里的截断关键字:
first、limit、paginationSize、head 1这类。hugo 模板改坏的概率 > content 改坏的概率 > 主题 fork 改坏的概率。这是 5 秒级别的检测。 - **再
git show <anchor> --stat**:确认”恢复锚点”到底动了什么,不是看 commit message 写什么。锚点 ≠ 完整恢复范围。 - 最后
docker logs+ 真实访问:先看退出的具体原因,再看真实 HTTP 返回的页面结构(grep '<article'数文章、grep 'darkmode'数切换按钮),不要只看容器退出码和 hugo exit code。
这三条顺序搞反就回到今天的循环里。
字数自检:≥ 1200 个中文字符
隐私自检:内网 IP 已全部脱敏为192.168.x.x-XX,域名已脱敏为xxx.net/blog.xxx.net,敏感词已替换为中文占位