豆备 DOUBAK
← 开发日志

双击 index.html,那就是你的豆瓣

站点生成器做完了,sample.doubak.com 上线。以及一个把用户自己写的字悄悄吃掉的 bug。

整条链路今天第一次从头通到尾:抓取 → 档案 → 解析 → 一个能打开的网站。 成品放在 sample.doubak.com, 用的是作者自己的豆瓣数据——2940 条标记、3394 条广播、4694 个页面、 2921 张封面加 124 张自己上传的图,一共 150 MB。

整个仓库下下来,双击 index.html 就能翻,不用起服务器,也不用联网。 页面里没有一个 src 指向站外——封面和图片的字节都从档案里导了出来。 这一条是整个项目的验收线:一份要豆瓣还在才能看的备份,不叫备份。

Markdown 才是产物,HTML 只是它的一个消费者

生成器输出的是 Markdown + YAML front matter,然后交给现成的静态站生成器渲染。 npm run md 出的那棵树里没有任何 Hugo 专属的东西(有一个测试专门守这条), 换 Astro / Eleventy / Jekyll 都能接。自带的 Hugo 骨架只有五个文件, 之所以只有五个,就是为了让它能被删掉——删了换任何一个现成主题, 每一个主题生态都变成它的模板库。

Hugo 本身是按固定版本下载官方二进制、对着仓库里写死的 SHA-256 校验, 不是一个 npm 依赖。因为 hugo-bin 会带来几百个间接包, 而一个「重建需要几百个包」的存档站点,跟它自己的前提是矛盾的。

然后是那个真正要紧的 bug

用户写的短评、日记正文,是要放进 Markdown 页面正文的。而豆瓣的短评里满是 Markdown 的活跃字符。拿 2831 条本人写的文本量了一遍:62 条渲染出来和原文不一样, 其中两条直接丢内容

_(:з」∠)_            →  <em>(:з」∠)</em>   下划线被吃掉(24 处)
From <May December>  →  From                片名整个消失

第二条是要命的那种:页面上什么都不剩,没有任何痕迹说明这里曾经有字。 一个会悄悄改写你写过的东西的存档,比没有存档更糟——因为它看起来是可信的。

修法是转义,不是打开 unsafe = true——那等于在一个要发到 GitHub Pages 的站点上 执行来自用户文本的 HTML。转义规则拿真实的 Hugo 验了 18 个用例,逐字节对回去。 其中有一条我一开始推错了,被构建当场纠正: 有序列表要转义的是那个点,不是数字——CommonMark 不认转义的数字。

永远不要把链接规则写死在 Markdown 里

交叉链接写成 ../movie/123.md——一个指向文件的路径, 最终 URL 长什么样交给静态站生成器决定。 第一版写的是 [标题](/movie/123/),结果一打开 uglyURLs 就全断,在 file:// 下每个链接都跳到目录列表。

还有一条同族的:relativeURLsuglyURLs 必须一起开。 当时只开了前一个,然后我写下「双击 index.html 可以用」——页面确实打开了, 而里面每一个链接都是坏的。 验证导航要把链接解析到文件,不是看页面渲染出来没有。

发布前先演一遍

npm run deploy 把成品铺进一个仓库根目录,GitHub Pages 直接能发。 它做两件容易被忽略的事:

  • 预演。明确列出哪些内容会变成公开的。别人的名字和短评在广播、 日记回复里到处都是,对这个受众里的一部分人来说,有些还是政治敏感的—— 所以第三方内容默认排除,而且发布前你能先看清楚。
  • 把旧文件清干净(只保留 CNAME / LICENSE / README / .git)。留下来的旧页面是幽灵页: 有固定链接、打得开、看起来像真的,而数据里早就没有它了。
← 更新:一按「开始抓取」就报错 更早:解析器能跑了,以及两个只有换种跑法才会露出来的 bug →