
本文永久链接 – 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 主要做了三件事:
- 通过
mysqldump备份 WordPress 数据库 - 通过 WordPress 的 WXR 导出功能,导出全量文章和评论数据
- 打包
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 辅助编程工具,这个过程远比你想象的简单。
注:迁移后的博客可能依然有“未发现”的小问题,如果大家发现了,还烦请留言告知🙏。
✍️ 今日开放讨论:
- 你的博客目前使用什么系统?有没有考虑过迁移到静态站点?
- 在博客迁移过程中,你最担心的是什么?
- 你使用 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 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

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