题图

本文永久链接https://tonybai.com/2026/06/27/wp-to-hugo-migration-journey

大家好,我是Tony Bai。

2026 年 6 月,我做了一件"蓄谋已久"的事——在AI 的帮助下,将运行了 15 年的 WordPress 博客迁移到了 Hugo 静态站点生成器。这不是一个心血来潮的决定,而是一个技术人对自己"数字花园"的一次深思熟虑的重构。

今天这篇文章,我想聊聊这次迁移的规划方法论、执行过程,以及那些在迁移过程中遇到的坑和解法。如果你也在考虑迁移博客,或者对"如何用 AI 智能体辅助完成一个复杂工程任务"感兴趣,这篇文章应该能给你一些启发。

一段 20 年的博客编年史

在聊迁移之前,先简单回顾一下我的博客"进化史"。

BlogBus 时代(2004-2011)

2004 年,我在 BlogBus(博客大巴)上开设了我的第一个技术博客。那个年代,博客是技术人分享知识的主要阵地,BlogBus 是国内为数不多的支持技术内容的托管平台。写博客不需要关心服务器、不需要懂运维,注册账号就能开始写。

然而,托管平台的命运终究不在自己手中。2011 年前后,BlogBus 逐渐式微,平台的不确定性让我决定:是时候拥有一个完全属于自己的博客了

自建 WordPress 时代(2011-2026)

2011 年,我在一台 VPS 上搭建了自己的 WordPress 博客。选择 WordPress 的原因很简单:它是当时最成熟的博客系统,生态丰富,插件众多,开箱即用。

在这 15 年间,WordPress 忠实地记录了我的技术成长——从 C/C++ 到 Go 语言,从传统软件开发到云原生架构,累计近 2,000 篇文章、几千条评论、数千张图片,数据量约 2.6 GB

期间值得一提的里程碑是 HTTPS 的启用。借助 Let’s Encrypt 提供的免费自动化证书,我的博客很早就实现了全站 HTTPS 访问,这在当年还算是一件比较"前卫"的事。

为什么要在 2026 年迁移?

既然 WordPress 跑了这么多年,为什么还要折腾迁移?原因有几个:

1. 性能与安全焦虑

WordPress 是一个动态系统,每次页面请求都需要 PHP 执行 + MySQL 查询。对于一个以"阅读"为主的技术博客来说,这种动态渲染是多余的。更关键的是,WordPress 是黑客最喜欢的攻击目标——即使你持续地更新版本和插件,安全漏洞的风险始终存在。我的WP长期停留在3.2.1版本,漏洞和被攻破的情况,时常发生。

2. 维护成本

PHP 版本升级、插件兼容性、数据库优化、缓存配置……运维 WordPress 就像养一盆需要定期浇水的植物。而静态站点几乎是"免维护"的——构建一次,到处运行。

3. 写作体验

Markdown 已经成为技术人写作的事实标准。在 WordPress 中写 Markdown 需要插件支持,体验总是隔靴搔痒。而 Hugo 天然拥抱 Markdown,配合 Git 版本管理,写作流程可以做到本地写、Git 推、自动部署

4. 掌控感

这一点或许是最重要的。作为一个技术人,我对自己的数字内容有一种"洁癖式"的掌控欲——我希望每一篇文章都是一个纯文本文件,存在 Git 仓库里,不依赖任何数据库,不依赖任何运行时。即使有一天 Hugo 消失了,我的文章依然在那里,用任何文本编辑器都能打开。

迁移方法论:先规划,后执行

面对一个包含近 2,000 篇文章、数千条评论、近3个G图片的复杂迁移任务,我没有选择"边做边想",而是采取了先规划、后执行的策略。

与 Claude Code 的协作模式

这次迁移,我全程使用了 Claude Code(Anthropic 的 AI 编程助手)作为协作工具。整个协作模式是这样的:

第一步:描述目标

我告诉 Claude Code 我的迁移目标:

将 tonybai.com 从 WordPress 迁移到 Hugo(PaperMod 主题),保留所有文章、评论、图片,URL 兼容,部署到一台新的 VPS。

第二步:让 AI 生成迁移计划

Claude Code 分析了我的项目结构、数据量和需求后,生成了一份迁移任务清单——8 个 Phase,40 个 Task,每个 Task 都有明确的输入、输出和验收标准。

第三步:逐 Phase 执行

每个 Phase 开始前,我会让 Claude Code 生成该 Phase 的详细步骤文档(phaseN-steps.md),包括具体的命令、代码和注意事项。然后我们逐步执行,遇到问题时 Claude Code 会分析日志、定位原因、提供修复方案。

这种模式的好处在于:AI 负责规划和细节记忆,人负责决策和验收。我不需要记住 Waline 的 SQLite 表结构,也不需要记住 Caddy 的反向代理配置语法——这些"知识密集型"的部分交给 AI,我只需要关注"这是否符合我的预期"。

8 个 Phase 的全景图

以下是 Claude Code 为我规划的 8 个迁移阶段:

Phase 1: 数据备份与准备          ← 导出 WordPress 数据
Phase 2: Hugo 站点初始化         ← 搭建 Hugo 骨架 + PaperMod 主题
Phase 3: 内容格式转换工具开发     ← 编写 Go 程序批量转换文章
Phase 4: 资源迁移               ← 图片文件迁移与路径替换
Phase 5: 本地构建与验证          ← 构建站点 + URL 兼容性验证
Phase 6: Waline 评论系统部署     ← 替代 WordPress 评论
Phase 7: VPS 部署               ← Caddy + Hugo + DNS 切换
Phase 8: 上线后收尾              ← 写作流程、模板、统计、社交图标等

每个 Phase 之间有明确的依赖关系(如下面Claude Code生成的依赖图)。比如 Phase 3 的内容转换依赖 Phase 1 的 WXR 导出,Phase 7 的部署依赖 Phase 5 的本地验证通过。这种流水线式的执行方式,让整个过程有条不紊。

Phase 1(数据准备)
  T-1.1 ──→ T-1.4
  T-1.2 ──→ (备用)
  T-1.3 ──→ T-3.1
  T-1.5 ──→ T-4.2

Phase 2(Hugo 站点初始化)        Phase 3(内容转换)
  T-2.1 → T-2.2 → T-2.3            T-3.1 → T-3.2 → T-3.3
                 ├→ T-2.4                      │
                 ├→ T-2.5                      ↓
                 └→ T-2.6                   T-4.1 → T-4.2 → T-4.3
Phase 5(本地验证)                              │
  T-3.3 + T-4.2 → T-5.1 → T-5.2 ←────────────┘
                          ├→ T-5.3
                          ├→ T-5.4
                          └→ T-5.5

Phase 6(Waline 评论)
  T-6.1 → T-6.2 → T-6.3
              └→ T-6.4 → T-6.5 → T-6.6
  T-1.4 ──────→ T-6.4

Phase 7(VPS 部署上线)
  T-7.1 + T-7.2 → T-7.3 → T-7.4 → T-7.5 → T-7.6 → T-7.7
  T-6.3 ──────────→ T-7.3
  T-5.1 ─────────────────→ T-7.4

Phase 8(收尾)
  T-7.6 → T-8.1, T-8.2
  T-6.3 → T-8.3
  T-7.5 → T-8.4
  T-6.5 → T-8.5

Phase 1-2:数据备份与站点初始化

这两个阶段相对标准化。

Phase 1 主要做了三件事:

  1. 通过 mysqldump 备份 WordPress 数据库
  2. 通过 WordPress 的 WXR 导出功能,导出全量文章和评论数据
  3. 打包 wp-content/uploads 目录(近5000 张图片,2.6 GB)

Phase 2 搭建 Hugo 骨架:

hugo new site tonybai-blog
cd tonybai-blog
git submodule add https://github.com/adityatelange/hugo-PaperMod themes/hugo-PaperMod

选择 PaperMod 主题的原因是:它对中文(CJK)支持良好、设计简洁、维护活跃、性能优秀,也是hugo生态排行第一的Theme。

在这个阶段,有一个关键配置决策——permalink 结构:

permalinks:
  posts: "/:year/:month/:day/:slug/"

这个配置确保 Hugo 生成的 URL 路径与 WordPress 完全一致(/2026/06/26/my-article/),从而保证所有外部链接不会失效。这是整个迁移中最重要的决策之一。

Phase 3:内容转换——用 Go 写一个批量转换器

这是整个迁移中技术含量最高的阶段。近2000 篇文章需要从 WordPress 的 HTML 格式转换为 Hugo 的 Markdown + YAML Front Matter 格式。

我没有选择手动转换(显然不现实),也没有使用现成的转换工具(灵活性不够),而是让 Claude Code 帮我用 Go 编写了一个专用的转换器

转换器的核心逻辑

WXR (XML) → 解析每篇文章 → 提取标题/日期/slug/标签/内容
          → 生成 YAML Front Matter
          → 将 HTML 内容转为 Markdown
          → 替换图片路径
          → 写入 .md 文件

遇到的坑

坑 1:标题格式不统一

我的文章标题有两种格式——早期是 Title: 文章标题 | Tony Bai,后期变成了 Title: 文章标题 - Tony Bai。转换器需要同时处理这两种分隔符:

// 处理两种标题格式: "| Tony Bai" 和 "- Tony Bai"
re := regexp.MustCompile(`\s*[\|]\s*Tony\s*Bai\s*$|\s*-\s*Tony\s*Bai\s*$`)
title = re.ReplaceAllString(rawTitle, "")

坑 2:2025 年后的文章"尾巴"问题

2025 年后的文章导出内容中,混入了数百行的网页模板噪音——评论表单、侧边栏、月度归档列表等。这些内容出现在正文之后,如果不清理,会全部混入 Markdown 文件。

解决方案是在版权标记处截断:

// 在版权标记处截断,清除后续模板噪音
cutoffRe := regexp.MustCompile(`\n© \d{4},`)
if loc := cutoffRe.FindStringIndex(content); loc != nil {
    content = content[:loc[0]]
}

坑 3:图片路径批量替换

WordPress 中所有图片的引用格式是 https://tonybai.com/wp-content/uploads/...,需要统一替换为 Hugo 的静态路径 /images/wp-content/uploads/...。这个替换在转换器中自动完成。

Phase 4-5:资源迁移与本地验证

Phase 4 处理图片迁移。近3个G的图片需要从 VPS 迁移到 Hugo 项目的 static/images/wp-content/uploads/ 目录下。

Phase 5 是关键的验证阶段。我用 Go 编写了一个 verify-urls 工具,自动对比 WordPress sitemap 中的 URL 与 Hugo 构建输出的 HTML 文件——最终验证通过率为 99.3%。剩余 0.7% 的差异主要是标签页和分类页的路径不同(Hugo 使用 /tags/xxx/ 而非 /tag/xxx/),可以通过重定向规则解决。

Phase 6:Waline 评论系统——最大的"坑"

评论系统的迁移是整个过程中最曲折的部分。

我选择了 Waline 作为评论系统——它是一个轻量级的自托管评论系统,后端使用 Node.js,数据库支持 SQLite,非常适合个人博客。

Waline 的 Docker 部署

services:
  waline:
    image: lizheming/waline:latest
    container_name: waline
    restart: always
    network_mode: host
    volumes:
      - ./data:/app/data
    environment:
      - SITE_NAME=Tony Bai
      - SITE_URL=https://tonybai.com
      - AUTHOR_EMAIL=xxx@xxx.com
      - JWT_TOKEN=xxxxx
      - SQLITE_PATH=/app/data

踩坑记录

坑 1(致命级):Waline Docker 不会自动创建 SQLite 表

这是我遇到的最大的坑。启动 Waline 容器后,SQLite 数据库文件被创建了,但打开一看——是 0 字节。没有表结构,什么都没有。

翻阅 Waline 源码后发现:Docker 镜像中的 SQLite 适配器只负责"连接"数据库,不负责"初始化"表结构。这意味着首次部署时必须手动创建表

我从 Waline 的 GitHub 仓库找到了官方 schema,手动执行了建表语句:

CREATE TABLE "wl_Users" (...);
CREATE TABLE "wl_Comment" (...);
CREATE TABLE "wl_Counter" (...);

坑 2:Docker 端口映射不生效

默认情况下,Docker 容器内的 Waline 绑定到 127.0.0.1(而非 0.0.0.0),导致端口映射 (ports: "8360:8360") 无法工作。

解决方案是使用 network_mode: host,让容器直接使用宿主机的网络栈。

坑 3:SQLite 数据库锁

导入评论数据时,如果 Waline 容器正在运行,会报 database is locked 错误。需要先停止容器,导入数据,再重启:

docker compose stop
./import-comments -input comments.json -db /opt/waline/data/waline.sqlite
docker compose start

坑 4:评论父子关系映射

WordPress 的评论有嵌套回复,使用 comment_parent 字段关联。导入 Waline 后,由于 Waline 使用自增 ID,原来的 WordPress 评论 ID 不再有效。

我的导入工具在导入过程中维护了一个 wpIDToRowID 映射表,将 WordPress 评论 ID 映射到 Waline 的新行 ID,确保嵌套关系正确。

评论导入结果

最终成功导入了全部存量评论内容,所有父子关系完整保留。

Phase 7:VPS 部署与 DNS 切换

Phase 7 是将 Hugo 站点部署到 VPS 的最后冲刺。

Caddy 生产配置

我使用 Caddy 作为 Web 服务器,生产环境的 Caddyfile 核心配置如下:

tonybai.com {
    root * /var/www/tonybai-blog/public

    # Waline 评论反向代理
    handle /waline/* {
        uri strip_prefix /waline
        reverse_proxy localhost:8360
    }

    # WordPress /feed/ 兼容重定向
    redir /feed/ /index.xml 301

    # 静态文件服务
    handle {
        file_server
    }

    # 静态资源长缓存
    @static path *.css *.js *.png *.jpg *.jpeg *.gif *.svg *.webp *.woff *.woff2
    header @static Cache-Control "public, max-age=31536000, immutable"

    # HTML 不缓存
    @html path *.html /
    header @html Cache-Control "public, max-age=0, must-revalidate"
}

几个值得注意的设计:

  • uri strip_prefix /waline:Waline 部署在 /waline 子路径下,Caddy 在转发前剥离这个前缀,让 Waline 以为自己运行在根路径
  • /feed//index.xml 重定向:WordPress 的 RSS 订阅地址是 /feed/,Hugo 的是 /index.xml,301 重定向确保老订阅者不会丢失
  • 分级缓存策略:静态资源(图片、CSS、JS)缓存 1 年(immutable),HTML 不缓存(确保更新即时生效)

DNS 切换前的验证

在 DNS 切换之前,我用 curl --resolve 模拟域名请求来验证配置:

curl -sk --resolve tonybai.com:443:127.0.0.1 "https://tonybai.com/" | head -20

确认一切正常后,将 tonybai.com 的 A 记录从旧 VPS IP 指向新 VPS IP。由于使用了 Cloudflare 代理,DNS 传播几乎是即时的。

Phase 8:上线后的收尾工作

DNS 切换完成后,还有一些收尾工作:

  • 文章模板(Archetype):创建了 archetypes/posts.md,预设 Front Matter 字段,hugo new 一篇新文章时自动填充模板
  • 访问统计:通过 PaperMod 的 extend_head.html 扩展点集成 StatCounter
  • 社交图标:PaperMod 内置了 100+ 社交图标 SVG,只需在 hugo.yml 中配置即可
  • Waline 管理:配置 SMTP 邮件通知、设置定时备份脚本(Git 提交到独立仓库)
  • Sitemap 提交:向 Google Search Console、Bing Webmaster、百度站长平台提交 sitemap

图片管理策略

2.6 GB 的图片数据不适合放在 Git 仓库中。我评估了四种方案:

方案 优点 缺点
A. 单仓库 简单 仓库过大,Git 操作缓慢
B. Git LFS 透明 GitHub 免费额度不足
C. 独立图片仓库 职责分离 需要维护两个仓库
D. rsync + Cloudflare R2 不依赖 Git 需要配置同步工具

最终我选择了方案 D:本地电脑作为图片来源,通过 rsync 同步到 VPS,同时用 rclone 定期备份到 Cloudflare R2(免费 10 GB 存储,无出口流量费)。三份副本,安心。

迁移成果一览

指标 数据
文章数量 1,955 篇
评论数量 2,219 条(622 篇文章)
图片数量 4,667 张(2.6 GB)
URL 兼容率 99.3%
构建时间 ~15 秒(Hugo --minify
页面加载 纯静态,毫秒级响应
迁移耗时 约 2 天

几点经验总结

1. 规划比执行更重要

面对复杂任务,花 20% 的时间做规划,能节省 80% 的返工时间。让 AI 生成迁移计划,然后人工审核和调整,是目前最高效的协作模式。

2. 自动化验证是必须的

近2000 个 URL 的兼容性验证,人工检查是不现实的。用 Go 编写一个自动验证工具,不仅能节省时间,还能在后续修改时反复运行。

3. URL 兼容性是第一优先级

对于一个运营了 20 年的博客,外部链接(搜索引擎、社交媒体、其他博客的引用)是宝贵的"数字资产"。permalink 结构保持一致,是整个迁移中最重要的决策。

4. 评论迁移是最容易踩坑的部分

评论系统涉及数据库 schema、数据格式转换、嵌套关系映射等多个技术点。Waline 的 SQLite 不自动建表这个问题,如果没有 AI 帮忙分析源码,可能会耗费大量排查时间。

5. AI 辅助编程的真正价值

这次迁移中,Claude Code 的最大价值不是"帮我写代码",而是:

  • 知识检索:Waline 的表结构、Caddy 的反向代理语法、PaperMod 的扩展点——这些"用完即忘"的知识,AI 信手拈来
  • 规划能力:将模糊的需求拆解为可执行的步骤
  • 问题诊断:遇到错误时,AI 能快速分析日志、定位原因、提供修复方案

小结

从 2004 年在 BlogBus 上写下第一篇技术文章,到 2026 年将博客迁移到 Hugo,这 20 年的博客历程,也是互联网技术变迁的一个缩影。

托管平台 → 自建 WordPress → 静态站点,这条路径的背后,是一个技术人对内容自主权技术掌控感的持续追求。

Hugo 静态站点不是终点,但它可能是目前最适合个人技术博客的形态:快速、安全、可控、低成本。更重要的是,它让我重新找回了"写博客"的纯粹感——打开编辑器,写 Markdown,Git push,完事。

如果你也有一个"年久失修"的 WordPress 博客,现在是时候考虑迁移了。借助 AI 辅助编程工具,这个过程远比你想象的简单。

注:迁移后的博客可能依然有“未发现”的小问题,如果大家发现了,还烦请留言告知🙏。


✍️ 今日开放讨论:

  1. 你的博客目前使用什么系统?有没有考虑过迁移到静态站点?
  2. 在博客迁移过程中,你最担心的是什么?
  3. 你使用 AI 辅助编程完成过什么复杂的工程任务?

欢迎在评论区分享你的经验。


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。