Migrating Feld Ideas from WordPress to Hugo – Cyber Tech

I’ve been running a blog (first on TypePad, however shortly after on WordPress) since 2004. That’s 22 years of posts – 5,530 of them – plus books, movies, operating logs, and occasions. WordPress served me properly for a very long time. Each few years I’d spend cash on a guide to redo the theme and construction of the positioning, however every time it was extra difficult (and costly) than the final time. And each time I wished to vary one thing on the positioning, it was more and more troublesome and fragile.

Given how deep I’ve gone into Claude Code, I began exploring totally different approaches for managing web-facing content material. As a part of determining the AuthorMagic web site creator, I found Hugo
. I experimented utilizing it for AdventuresInClaude
and favored it, so – I made a decision to see if I may use Claude Code to do a full migration of Feld Ideas to Hugo.

A day later, Feld Ideas has a brand new residence. Whereas the theme is easy to start out, I’ve full management over it and can iterate on it (in Claude Code) to get it in a type that I like.


The goal stack is simple. Hugo generates a static web site from markdown information. Vercel hosts it. I’ve been spending numerous time with Markdown information these days and I get pleasure from working with them far more than something that requires formatting. No extra database, no PHP, and no WordPress updates. Simply markdown information in a git repo.

Step one within the course of was getting the content material out. WordPress has a built-in REST API at yoursite.com/wp-json/wp/v2. Claude wrote a TypeScript script that fetches all posts by way of paginated API calls, fetches all classes and tags, and converts HTML to Markdown utilizing the Turndown library. It strips WordPress block feedback, handles caption shortcodes, and decodes all of the HTML entities WordPress likes to scatter by means of titles and descriptions – good quotes, em dashes, ellipses.

The script makes use of a state file for resumable checkpoints. If it crashes or will get rate-limited, it picks up the place it left off. This turned out to be important.


A key choice was the content material construction. I used Hugo “web page bundles” to protect the WordPress URL construction:

content material/archives/2012/10/random-act-of-kindness-jedi-max/index.md

This maps on to feld.com/archives/2012/10/random-act-of-kindness-jedi-max/ – the identical URL WordPress used. Each previous hyperlink nonetheless works. No redirects wanted.

Hugo’s configuration makes this specific:

[permalinks.page]
  archives = "/archives/:12 months/:month/:slug/"

If in case you have {custom} put up sorts – I had books, movies, operating logs, and occasions – these want separate exports since they reside at totally different API endpoints. Every will get its personal content material listing.


The media obtain was difficult. WordPress CDN URLs are available a bunch of variants – i0.wp.com/feld.com, www.feld.com, direct feld.com paths, all with numerous question parameters. The media script scans each exported markdown file for these URLs, normalizes them, and downloads the precise pictures.

The intelligent half is the reference counting. Photographs utilized by just one put up get co-located in that put up’s web page bundle listing. Photographs shared by two or extra posts go to static/pictures/ with year-month prefixes to keep away from filename collisions. After downloading, it rewrites all of the markdown URLs from WordPress CDN paths to native relative paths.

A separate cleanup cross fixes HTML entities that survived within the frontmatter. WordPress shops stuff like & and ’ in titles and descriptions. These should be decoded to precise characters, then the YAML strings should be re-escaped correctly. That is the type of factor that sounds trivial however breaks in shocking methods when you could have 5,530 posts.


Claude wrote a verification script that fetches the WordPress sitemaps and compares each URL towards the Hugo content material listing. It reviews the match price and lists any lacking posts. We iterated till it hit 100% accuracy.

For the theme, I used PaperMod as a place to begin and forked it. The fork lets me customise branding, layouts, and options with out worrying about upstream updates. I added client-side search by way of Pagefind, which is important for a web site this dimension. Pagefind builds a static search index at construct time. I added data-pagefind-body to the only put up template so it solely indexes put up content material – not navigation, footers, or different chrome. This dropped the index from 10,000+ pages to about 5,500 and lower the search index construct time from 32 seconds to eight seconds.


Deployment is easy. Push the Hugo repo to GitHub, join it to Vercel, and level the area’s DNS to Vercel. Each git push to essential triggers a rebuild and deploy. My 5,530 posts construct in about 47 seconds. The entire deploy – clone, construct, CDN add – is beneath 3 minutes.

The DNS cutover required documenting each current document first – MX information for e-mail routing, SPF, DKIM, and DMARC for e-mail authentication, CAA information for SSL. I recreated all of them in Vercel DNS after the switch.

I additionally switched from Mailchimp to Package for e-mail subscribers. Package has RSS-to-email automation that watches the Hugo RSS feed and sends new posts to subscribers routinely. No API integration wanted.


For those who’re fascinated about doing this, three issues matter most. First, URL preservation is non-negotiable. Get the permalink construction proper from the beginning so each previous hyperlink works with out redirects. Second, the media obtain is the place issues get messy – WordPress CDN URLs are available many variants, and also you want reference counting to deal with shared pictures appropriately. Third, write a verification script and run it obsessively till you hit 100%.


I’ve open-sourced the migration scripts at github.com/bradfeld/wp-to-hugo
. They’re genericized – you configure your web site URL and {custom} put up sorts in a single JSON file and the scripts deal with the remaining.

The toolkit has 5 scripts, meant to be run so as:

  1. wp-export – Fetches all posts and pages by way of the WP REST API, converts HTML to Markdown, and writes Hugo web page bundles with correct frontmatter (classes, tags, descriptions).
  2. export-custom-types – Exports {custom} put up sorts (books, movies, no matter your web site has) to separate content material directories.
  3. wp-media-download – Scans all exported markdown for WordPress media URLs, downloads the pictures, and rewrites the URLs to native paths. Handles the reference counting (single-use pictures go within the web page bundle, shared pictures go to static/pictures/).
  4. fix-entities – Cleans up HTML entities that WordPress shops in titles and descriptions (&, ’, good quotes, and many others.).
  5. wp-verify – Fetches your WordPress sitemap and compares each URL towards the Hugo content material listing. Run this till you hit 100%.

To make use of the scripts you’ll want Node.js
(model 20+) and a WordPress web site with the REST API enabled (most have it on by default – verify by visiting yoursite.com/wp-json/wp/v2/posts). You’ll additionally want Hugo
put in to construct the positioning, a GitHub
repo to retailer it, and a internet hosting platform like Vercel
or Netlify
to serve it. The scripts deal with the content material migration – organising Hugo, selecting a theme, and configuring deployment is on you, however Hugo’s fast begin information
covers most of it.

The scripts are resumable – if one crashes or will get rate-limited, re-run it and it picks up the place it left off.

The repo’s documentation
has an in depth walkthrough of how every part works, together with the media URL normalization technique and the reference counting logic.


Claude Code did all of the heavy lifting. I described what I wished and it wrote the scripts, configured Hugo, arrange the theme, and dealt with deployment.

Add a Comment

Your email address will not be published. Required fields are marked *

x