Operations 20 min read

How I Migrated a 20‑Year‑Old WordPress Blog to Hugo with AI Assistance

In June 2026, the author migrated a 15‑year‑old WordPress blog containing nearly 2,000 posts, 2,200 comments and 2.6 GB of images to a Hugo static site, using Claude Code for planning, a custom Go converter, Dockerized Waline for comments, and Caddy on a VPS, achieving 99.3% URL compatibility in about two days.

TonyBai
TonyBai
TonyBai
How I Migrated a 20‑Year‑Old WordPress Blog to Hugo with AI Assistance

Background

The blog accumulated ~1,955 articles, 2,219 comments and 2.6 GB of images (≈4,667 files) over 15 years.

Why migrate to Hugo

Performance & security: WordPress renders each request with PHP + MySQL, which is unnecessary for a read‑only blog and makes it a frequent attack target.

Maintenance cost: Keeping PHP, plugins and the database up‑to‑date requires continual effort.

Writing experience: Markdown is the de‑facto standard for developers; Hugo natively supports it, enabling a local‑write → Git‑push → auto‑deploy workflow.

Control: Storing each article as a plain‑text file in a Git repository gives full ownership and future‑proofness.

Migration methodology (AI‑assisted planning)

Claude Code (Anthropic’s AI coding assistant) generated an 8‑phase plan with 40 tasks, each defined by inputs, outputs and acceptance criteria.

Phase 1 – Data backup

Export the MySQL database with mysqldump.

Export all posts and comments via WordPress’s WXR exporter.

Archive the wp-content/uploads directory (≈5,000 images, 2.6 GB).

Phase 2 – Hugo site initialization

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

The PaperMod theme was chosen for its CJK support, clean design and active maintenance. The permalink structure was set to match the original WordPress URLs:

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

Phase 3 – Content conversion (custom Go converter)

The converter parsed the WXR XML, extracted title, date, slug, tags and content, generated YAML front matter, converted HTML to Markdown and rewrote image URLs.

Key challenges and solutions:

Inconsistent title format: Early posts used "Title | Tony Bai" and later posts used "Title - Tony Bai". A regular expression handled both patterns:

// handle both "| Tony Bai" and "- Tony Bai"
re := regexp.MustCompile(`\s*[|]\s*Tony\s*Bai\s*$|\s*-\s*Tony\s*Bai\s*$`)
title = re.ReplaceAllString(rawTitle, "")

Post‑2025 template noise: Exported content contained footer HTML (comments form, sidebars). The converter truncated at the copyright marker using a regex:

// truncate at copyright marker
cutoffRe := regexp.MustCompile(`
© \d{4},`)
if loc := cutoffRe.FindStringIndex(content); loc != nil {
    content = content[:loc[0]]
}

Image path replacement: WordPress URLs like https://tonybai.com/wp-content/uploads/… were rewritten to /images/wp-content/uploads/….

Phase 4 – Resource migration

All images were copied to static/images/wp-content/uploads/ in the Hugo project.

Phase 5 – Local build & verification

A Go tool verify-urls compared the WordPress sitemap with the Hugo‑generated HTML files, achieving 99.3 % URL compatibility. The remaining 0.7 % differences were tag‑page path variations ( /tags/xxx/ vs /tag/xxx/) that can be fixed with redirects.

Phase 6 – Comment system (Waline)

Waline (Node.js + SQLite) replaced WordPress comments.

Docker schema issue: The container created an empty SQLite file; tables had to be created manually using the official schema from the Waline GitHub repository.

Port mapping: By default Waline bound to 127.0.0.1, breaking ports: "8360:8360". Setting network_mode: host resolved it.

SQLite lock: Importing comments required stopping the container, running the import script, then restarting.

Parent‑child mapping: WordPress comment IDs were mapped to new Waline row IDs via a wpIDToRowID table to preserve nesting.

Phase 7 – VPS deployment & DNS switch

Caddy served the site. Core configuration:

tonybai.com {
    root * /var/www/tonybai-blog/public
    handle /waline/* {
        uri strip_prefix /waline
        reverse_proxy localhost:8360
    }
    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 path *.html /
    header @html Cache-Control "public, max-age=0, must-revalidate"
}

Verification before DNS cut‑over used:

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

After confirming the response, the A record was switched to the new VPS IP via Cloudflare (near‑instant propagation).

Phase 8 – Post‑launch tasks

Created archetypes/posts.md to scaffold new articles.

Integrated StatCounter via PaperMod’s extend_head.html.

Configured social‑icon SVGs in hugo.yml.

Set up SMTP notifications and backup scripts for Waline.

Submitted sitemaps to Google, Bing and Baidu.

Image‑management strategy: chose rsync + Cloudflare R2 (option D) to keep three copies (local, VPS, R2) without inflating the Git repository.

Results

Articles: 1,955

Comments: 2,219 (across 622 posts)

Images: 4,667 (2.6 GB)

URL compatibility: 99.3 %

Build time: ~15 seconds (Hugo --minify)

Page load: static, millisecond‑level response

Migration duration: ≈2 days

Key takeaways

Planning outweighs execution: Spending ~20 % of time on a detailed plan saved ~80 % on rework; AI‑generated plans followed by human review proved most efficient.

Automated verification is essential: Manually checking ~2,000 URLs is infeasible; a custom Go verifier enabled repeatable checks.

URL compatibility is paramount: Preserving the exact permalink structure protected existing inbound links.

Comment migration is the biggest pitfall: Schema creation, port binding, SQLite locking, and ID remapping required careful handling.

AI‑assisted development adds real value: Claude Code provided knowledge retrieval (Waline schema, Caddy syntax, PaperMod hooks), broke down the migration into executable steps, and diagnosed errors quickly.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DockerGostatic site generatorWordPressHugoWalineCaddyAI-assisted migration
TonyBai
Written by

TonyBai

Tony Bai's tech world (tonybai.com). Not satisfied with just "knowing how", we strive for mastery. Focused on Go language internals, high-quality engineering practices, and cloud‑native architecture, exploring cutting‑edge intersections of Go and AI. Gophers who pursue technology are welcome—follow me and evolve with Go.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.