从 Halo 到 Hexo:一次博客静态化改造的完整记录
这个博客在 Halo 上跑了很久。它的编辑体验、插件体系和后台设计都很完整,我也确实用它写过不少东西。但用得越久,越能感觉到一件事:我需要的只是一个能稳定打开的个人博客,而它给我的是一整套需要维护的应用。
这不是 Halo 的问题,是我和它的使用场景不匹配。下面是这次迁移的完整记录,包括数据怎么处理、路由怎么兼容、评论怎么办,以及为什么最后选择了「静态页面 + 独立评论服务」这种看起来有点分裂的架构。
一、先说我遇到的问题
迁移之前,我认真地量了一遍现网站的表现,而不是凭感觉。
| 项目 | 实测情况 |
|---|---|
| 首页 TTFB | 1.8 s – 3.8 s,偶尔 30 s 超时 |
| 缓存策略 | Cache-Control: no-cache, no-store,每次都是完整回源 |
| RSS / Sitemap | 绝对地址写成了 http://<服务器 IP>:8090,订阅器和搜索引擎拿到的都是内网地址 |
| 常驻内存 | Java 进程 + MySQL + OpenResty,长期占用大半台 2 GB 内存的机器 |
| 数据存储 | 内容都在 MySQL 的 extensions 表里,JSON 加快照补丁链,导出要靠公开 API |
这些问题里,只有内存占用是「贵」,其余都是「慢」和「不对」。而慢的根因并不难猜:每个请求都要经过 Java 进程做模板渲染,又没有任何缓存,机器本身还不快。
真正让我下决心的是第三行——RSS 和 Sitemap 里的链接是错的。对一个博客来说,这等于对外发出的所有地址都不可用。
二、目标不是「换个框架」,是「减掉一层」
迁移前我给自己定了三条约束,避免把一次简化做成一次重写:
- 前端观感不变。 用同一套 Fluid 风格的页面,读者不应该感觉到搬家。
- 旧链接全部可用。 搜索引擎收录的和别人收藏的地址,必须 301 到新地址。
- 功能不缩水。 评论、搜索、RSS、友链、深色模式,一个都不能少。
架构上的变化只有一句话:把「每个请求都渲染」换成「构建时渲染一次,之后只读文件」。
区别发生在请求到达之后:旧架构里,OpenResty 把请求交给 Halo,Halo 查库、渲染、返回;新架构里,OpenResty 直接在磁盘上找到对应的 index.html 返回。评论是唯一需要后端的功能,单独拆成一个小服务,只在 /comment/ 这条路径上工作。
三、数据怎么搬
这是整个迁移里最需要小心的一步,因为 Halo 的数据不是「一篇文章一个文件」,而是存在数据库里的结构化记录。
我的做法是不碰原库,只用公开接口读取:
- 通过 Halo 的公开 API 拉取文章列表和正文;
- 逐篇转换成 Markdown,保留标题、日期、分类、标签和封面;
- 图片等附件按原路径下载到本地,改写成站内相对地址;
- 原来的草稿不迁移——草稿本来就不该出现在公开站点上。
转换完成后,我在本地逐篇对了一遍正文:代码块的语言标记、表格、内嵌 HTML、图片尺寸属性,这些是最容易在转换中丢失的东西。最终 6 篇文章里,5 篇正常发布,1 篇因为原本就是草稿而没有迁移。
一个经验:迁移前先把原站完整备份一遍,数据库导出、附件目录打包、容器配置和反向代理配置都留存。备份不是为了「应该备份」,而是为了在发现转换错误时,能立刻回去重新导一次。
四、旧链接怎么办
Hexo 默认的文章地址是 posts/<slug>/,而 Halo 用的是 /?p=<uuid>。直接换掉的话,所有外部链接都会 404。
处理方式是在反向代理层做一次永久重定向:
location = / {
if ($arg_p = "07725034-a8d9-4257-97aa-16d3f27f3938") {
return 301 /posts/CPP_01/;
}
if ($arg_p = "dc4b291d-f622-4eda-9d5c-fa5d4a7038be") {
return 301 /posts/cyu-yan-chang-yong-ku-han-shu/;
}
try_files /index.html =404;
}
每篇文章的 UUID 和新地址一一对应。这样做的代价是配置里多了一段映射表,好处是不需要在页面里塞 JavaScript 跳转,也不会影响首页本身。
同时把 Hexo 的 pretty_urls 关掉尾部 index.html:
pretty_urls:
trailing_index: false
trailing_html: false
这样对外暴露的地址是 /posts/xxx/ 而不是 /posts/xxx/index.html,和原来的观感一致,也更利于收录。
五、评论:唯一保留的后端
静态站点没有数据库,评论必须单独解决。我评估过几种方案:
- Disqus:接入最简单,但国内加载不稳定,还要把读者数据交给第三方;
- ** utterances / giscus**:基于 GitHub Issues,适合开发者,但要求读者有 GitHub 账号;
- Waline:开源自托管,支持匿名评论,数据在自己手里。
最后选了 Waline,用 SQLite 存储,部署成一个独立容器,通过 /comment/ 路径反向代理。它只处理评论的读写,和静态页面完全解耦——即使评论服务挂了,文章页依然能正常打开。
具体的部署和踩坑过程写在另一篇里:自托管 Waline 评论:SQLite + 反向代理的最小可用方案。
六、那些「顺手修掉」的细节
迁移过程中还修了几件在原站上一直存在、但没空处理的小问题:
- RSS 和 Sitemap 的地址:Hexo 生成时用的是
url配置,改成正式域名后,订阅器终于能拿到正确的链接; - 主题资源本地化:jQuery、Bootstrap、图标字体这些原本走公共 CDN,现在全部放在站内,减少外部依赖和首屏等待;
- 搜索结果转义:本地搜索对
C++这类带正则元字符的关键词会报错,补了一次转义; - 深色模式:跟随系统,同时保留手动切换,两套配色都实际截图核对过。
这些改动单个都不大,但叠在一起,才是「体验变好」而不是「换了个框架」。
七、结果
迁移完成后的对比:
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| 首页 TTFB | 1.8 – 3.8 s | 20 – 50 ms |
| 缓存 | 禁用 | HTML 10 分钟,静态资源 30 天 |
| 常驻内存 | Java + MySQL 占大头 | 静态文件 + 一个评论容器 |
| 发布方式 | 后台点发布 | 本地构建,上传静态文件 |
| 数据形态 | 数据库里的 JSON | Markdown 源文件 + Git |
代价也很明确:发布不再是一个按钮。写完文章要本地构建、上传,多了一步。对我来说这一步可以接受,因为它换来了可版本控制的源文件、可回滚的发布,以及一个几乎不会出故障的线上站点。
八、如果重来一次
有两点我会做得更早:
- 先量再改。 如果一开始就记录 TTFB、缓存头、资源数量,很多争论其实不需要发生;
- 先备份再动。 数据迁移的容错空间,完全取决于备份是否完整。
Halo 依然是一个好项目,只是它更适合「需要一整套内容平台」的场景。对我这种一年写不了几十篇、但希望每篇都能被稳定打开的个人博客来说,静态化是更合适的选择。