Migrating a 20-Year WordPress Blog to Hugo and Cloudflare
I moved my 20+ year old WordPress blog off Bluehost and onto a modern static site setup with Hugo and Cloudflare Workers. What I expected to be a painful, multi-week project turned into a surprisingly smooth 4-day migration, largely thanks to working with Claude as a coding agent throughout the process.
Why Move Away from WordPress?
WordPress has served me well for two decades, but it felt like overkill for what is essentially a personal blog. I’m not running e-commerce, I don’t need a database for every page load, and I definitely don’t need the constant plugin updates and security concerns. More importantly, Bluehost, while reliable, isn’t exactly a modern hosting vendor. I wanted to explore what a more current tech stack could offer.
This migration gave me an excuse to dive into Cloudflare’s ecosystem and see what they’re really capable of beyond just being a CDN. The cost savings (from ~$150/year to essentially free) was a nice outcome, but the real motivation was modernization and learning.
The Export Process: WordPress to Markdown
The first step was getting content out of WordPress. I used the Simply Static plugin to export everything as static HTML files. This worked well and gave me a complete snapshot of the blog including all images, styles, and 20 years of posts. I decided to keep that as the main archive.
But Hugo uses Markdown, not HTML. I used a Wordpress Export to extract all content into a download XML file. Then ran a WordPress Export to Markdown tool on the export file.
npx wordpress-export-to-markdown
That created individual index.md files for each post with proper front matter. Hugo is flexible about content structure. The WordPress Export to Markdown tool gave me options, and I decided to organize posts using a year/month hierarchy with page bundles:
content/
└── posts/
└── 2025/
└── 03/
└── exploring-llms-as-agents/
├── index.md
└── images/
└── screenshot.png
This page bundle approach meant each post lived in its own folder with all its assets. Images could use relative paths like images/screenshot.png in the Markdown, making posts completely self-contained.
I chose the rusty-typewriter theme as a starting point. Hugo’s theme system made it easy to customize without touching the theme files directly — just override partials in my own layouts/ directory when needed.
A few things to note: I added series as a custom taxonomy (great for multi-part posts), and configured a search index output so the theme’s built-in client-side search would work. The search feature was surprisingly fast once I optimized the index to only include post summaries instead of full content.
Fixing Legacy Content
Twenty years of content meant dealing with some interesting legacy issues:
Cross-Post Links
My WordPress URLs followed the pattern /blog/YYYY/MM/post-slug/, but I wanted cleaner Hugo URLs at /posts/YYYY/MM/post-slug/. Internal links needed updating:
# Find and replace across all Markdown files
find content/posts -name "*.md" -type f -exec sed -i '' 's|https://starkravingfinkle.org/blog|/posts|g' {} +
Ancient Blogspot Links
I found posts that still linked to my really old Blogspot blog from 2004. Those needed fixing too:
# Handle multiple URLs on same line (trickier regex)
find content/posts -name "*.md" -type f -exec sed -i '' 's|http://weborama\.blogspot\.com/\([^)]*\)\.html|/posts/\1/|g' {} +
The key was using [^)] in the regex pattern to stop at the closing parenthesis of Markdown links, preventing the pattern from being too greedy when multiple links appeared on one line.
WordPress Shortcodes
WordPress uses [caption] shortcodes that Hugo doesn’t understand. I converted them to Hugo’s native figure shortcode.
I did some of these conversions manually for the handful of captioned images, but Claude created a script would work for larger volumes.
Deploying to Cloudflare Workers
Getting Hugo working locally was one thing; deploying to Cloudflare Workers was where I got to learn new things. Workers can serve static sites directly, which is perfect for a Hugo blog.
Basic Worker Setup
mkdir starkravingfinkle-blog
cd starkravingfinkle-blog
npm init -y
npm install wrangler --save-dev
The wrangler.toml configuration started simple:
name = "starkravingfinkle-blog"
compatibility_date = "2025-02-04"
[assets]
directory = "./public"
not_found_handling = "404-page"
binding = "ASSETS"
Hugo builds into the public/ directory, and Workers serves from there. The binding = "ASSETS" creates an environment variable you can reference in Worker code.
Handling URL Redirects
Here’s where it got more complex. My old WordPress URLs used /blog/ in the path, but I wanted the Hugo site at the root. Breaking existing links was not an option. There has to be a few people who have linked to my posts over 20 years.
I needed a Worker script to handle redirects:
// src/index.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Redirect /blog/ paths to /posts/
if (url.pathname.startsWith('/blog/')) {
// Handle RSS feed
if (url.pathname === '/blog/feed/' || url.pathname === '/blog/feed') {
return Response.redirect(url.origin + '/feed.xml', 301);
}
// Handle blog posts
const newPath = url.pathname.replace('/blog/', '/posts/');
return Response.redirect(url.origin + newPath, 301);
}
// Serve static assets normally
return env.ASSETS.fetch(request);
}
};
Updated wrangler.toml to use the script:
name = "starkravingfinkle-blog"
main = "src/index.js"
compatibility_date = "2025-02-04"
[assets]
directory = "./public"
not_found_handling = "404-page"
binding = "ASSETS"
Now old WordPress URLs redirect to the new Hugo structure with proper 301 permanent redirects.
Deployment
The deployment workflow is simple:
# Build the Hugo site
hugo
# Deploy to Cloudflare
npx wrangler deploy
That’s it. The site is live on Cloudflare’s edge network in seconds.
DNS and Domain Migration
Moving from Bluehost to Cloudflare meant changing nameservers and setting up custom domains. The process was straightforward:
- Add domain to Cloudflare (it scans existing DNS)
- Review imported DNS records
- Update nameservers at domain registrar
- Add custom domain to Worker (automatic DNS setup)
- Update
baseURLinhugo.toml - Rebuild and redeploy
DNS propagation took about 30 minutes in my case. I tested first with the *.workers.dev subdomain before pointing the real domain, which gave me confidence everything worked.
One gotcha: Browser DNS caching. Even after DNS propagated globally, my main browser held onto the old Bluehost IP for hours. Testing in Safari (not my main browser) showed the new site was live while Firefox stubbornly showed the old one. Clearing Firefox’s DNS cache (about:networking#dns) fixed it.
Search Functionality
The rusty-typewriter theme includes client-side search using a JSON index. Initially, the search was slow because the index included full post content. Over 20 years of writing made for a huge JSON file.
The fix was simple: generate summaries instead of full content in the search index template:
{{- $.Scratch.Add "searchindex" slice -}}
{{- range $index, $element := where site.RegularPages "Params.indexable" "ne" false -}}
{{- $.Scratch.Add "searchindex" (dict
"id" $index
"title" $element.Title
"permalink" $element.RelPermalink
"tags" (delimit ($element.Params.tags | default slice) " ")
"summary" ($element.Summary | plainify)
"date" ($element.Date.Format ($.Param "dateformat" | default "2 January, 2006"))
) -}}
{{- end -}}
{{- $.Scratch.Get "searchindex" | jsonify -}}
Removing the full .Plain content and using summaries made the search index 80-90% smaller and noticeably faster, but still gave very good search results
Working with Claude as a Coding Agent
I need to call out how much easier this migration was because I worked with Claude throughout. I’m not talking about asking occasional questions, I mean having Claude actively participate in the entire migration as a coding agent.
What Claude helped with:
- Explaining Hugo’s content structure and conventions
- Writing and debugging sed commands for bulk URL fixes
- Creating the Worker redirect logic
- Troubleshooting DNS and configuration issues
- Optimizing the search index
- Walking through Cloudflare setup step-by-step
The biggest value wasn’t just getting answers, it was the iterative problem-solving. When something didn’t work, we’d debug together. When I hit a wall, Claude suggested alternatives. When I learned something new about Hugo or Cloudflare, Claude helped connect it to the bigger picture.
This is what AI coding agents excel at: removing the friction of getting started and maintaining momentum when you’re learning multiple new technologies at once. Without Claude, I probably would have spent a week just reading Hugo documentation and Cloudflare guides before writing any code. It might have been enough perceived friction that I just wouldn’t even have started.
Learnings
Static Sites Scale Down Complexity
Moving from WordPress to static HTML eliminated an entire class of concerns: database performance, PHP updates, plugin conflicts, security patches. The site is literally just files being served from Cloudflare’s edge network. Simple is powerful.
Redirects Were Important
Twenty years of links exist out there in the wild. Search engine indexes, social media posts, bookmarks, other blogs — all pointing to old URLs. The redirect strategy needed to be bulletproof. Cloudflare Workers made this trivial with a few lines of JavaScript.
AI Agents Change the Learning Curve
Learning Hugo, Cloudflare Workers, DNS setup, and email configuration simultaneously would normally take weeks. With Claude as an active partner, it took 4 days. The agent didn’t just answer questions, it helped me make decisions, debug problems, and connect concepts across multiple technology stacks.
Legacy Content Has Character
Finding 20-year-old Blogspot links in my posts was a fun reminder of how long I’ve been writing online. Rather than being annoyed by the cleanup work, I appreciated the archeology of it. Every odd WordPress shortcode or malformed URL told a story about what blogging tools were like in 2005.
What’s Next?
Now that the blog is running on Hugo and Cloudflare, I want to explore a few additions:
New content types: Hugo makes it easy to add distinct content types beyond blog posts. I’m thinking about adding quotes and TIL (Today I Learned) sections — short-form content that doesn’t fit the typical blog post format.
Writing workflow: Since the blog content is now local Markdown files (not trapped in a WordPress database), I can work more easily on drafts. The friction of “opening WordPress to write” is gone. It’s just files in a directory.
Analytics refinement: Cloudflare Web Analytics is running, but I want to understand what reports and insights are actually useful versus vanity metrics. The data is there; I need to figure out what questions to ask of it.
Final Thoughts
Migrating a 20-year blog sounds daunting, but it turned out to be one of those projects that’s easier than you think, especially with the right tools. Hugo is powerful but approachable, Cloudflare Workers are remarkably simple to deploy to, and working with Claude as a coding agent removed most of the friction from learning new technologies.
If you’re on the fence about moving away from WordPress, I’d encourage you to try it. Static sites aren’t just faster and cheaper, they’re genuinely simpler to reason about. And with modern tooling (Hugo’s live reload, Wrangler’s instant deploys, AI agents for guidance), the developer experience is excellent.
The web has come a long way since I started this blog in the early 2000s. It’s nice to have my infrastructure finally catch up.