从 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 里的链接是错的。对一个博客来说,这等于对外发出的所有地址都不可用。

二、目标不是「换个框架」,是「减掉一层」

迁移前我给自己定了三条约束,避免把一次简化做成一次重写:

  1. 前端观感不变。 用同一套 Fluid 风格的页面,读者不应该感觉到搬家。
  2. 旧链接全部可用。 搜索引擎收录的和别人收藏的地址,必须 301 到新地址。
  3. 功能不缩水。 评论、搜索、RSS、友链、深色模式,一个都不能少。

架构上的变化只有一句话:把「每个请求都渲染」换成「构建时渲染一次,之后只读文件」。

Halo 与 Hexo 架构对比

区别发生在请求到达之后:旧架构里,OpenResty 把请求交给 Halo,Halo 查库、渲染、返回;新架构里,OpenResty 直接在磁盘上找到对应的 index.html 返回。评论是唯一需要后端的功能,单独拆成一个小服务,只在 /comment/ 这条路径上工作。

三、数据怎么搬

这是整个迁移里最需要小心的一步,因为 Halo 的数据不是「一篇文章一个文件」,而是存在数据库里的结构化记录。

我的做法是不碰原库,只用公开接口读取:

  1. 通过 Halo 的公开 API 拉取文章列表和正文;
  2. 逐篇转换成 Markdown,保留标题、日期、分类、标签和封面;
  3. 图片等附件按原路径下载到本地,改写成站内相对地址;
  4. 原来的草稿不迁移——草稿本来就不该出现在公开站点上。

转换完成后,我在本地逐篇对了一遍正文:代码块的语言标记、表格、内嵌 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

代价也很明确:发布不再是一个按钮。写完文章要本地构建、上传,多了一步。对我来说这一步可以接受,因为它换来了可版本控制的源文件、可回滚的发布,以及一个几乎不会出故障的线上站点。

八、如果重来一次

有两点我会做得更早:

  1. 先量再改。 如果一开始就记录 TTFB、缓存头、资源数量,很多争论其实不需要发生;
  2. 先备份再动。 数据迁移的容错空间,完全取决于备份是否完整。

Halo 依然是一个好项目,只是它更适合「需要一整套内容平台」的场景。对我这种一年写不了几十篇、但希望每篇都能被稳定打开的个人博客来说,静态化是更合适的选择。


从 Halo 到 Hexo:一次博客静态化改造的完整记录
https://stone.us.kg/posts/halo-to-hexo
作者
Mark Luo
发布于
2026年11月04日
许可协议