The WordPress install on this site had gotten into a state. The wrong theme was active. Plugins were running that I didn’t recognize. Security updates had queued up while I was busy with other things. At some point I’d stopped caring, which is usually when something breaks.
I decided to move to Hugo. This is how I did it, and what broke along the way.
Why Hugo
Static HTML doesn’t need a database, PHP version upgrades, or plugin maintenance. Hugo generates a directory of HTML files from Markdown and templates. You deploy that directory to a web server. Done.
I looked at Ghost and Gatsby. Ghost has a managed hosting option but costs money for something I can run myself. Gatsby is fine if you like JavaScript everywhere. Hugo has been around since 2013, has very few dependencies, and builds fast enough that I never notice the build time.
The theme
I wanted the site to keep the same look. The original WordPress Tabor theme has a Hugo port: hugo-theme-tabor. It was close enough.
Clone it into themes/tabor/, run npm install && yarn build to compile the CSS, and set theme = "tabor" in your config.
Exporting content
The WordPress to Hugo Exporter plugin handles most of the work. Install it, click Export to Hugo, unzip. You get a posts/ directory with Markdown files and a wp-content/ folder with your uploads.
The front matter needs some cleanup. The exporter uses featured_image but the Hugo Tabor theme looks for cover. It also leaves WordPress-specific fields like id, user, and iicsv_post_slug that Hugo ignores. I wrote a short Python script to rename featured_image to cover, strip the WordPress fields, and convert absolute image URLs to root-relative paths.
That URL conversion matters for local development. The exporter writes paths as https://yoursite.com/wp-content/uploads/.... Those work on the live site but not locally. A quick sed across all your content files fixes it:
find content/ -name "*.md" -exec sed -i '' 's|https://yourdomain.com/wp-content/|/wp-content/|g' {} +
Copy the wp-content/ folder into static/ and your images will be accessible at /wp-content/uploads/....
Things that broke
Images were invisible. The Tabor theme’s lazy loading CSS sets .entry-content img { opacity: 0 } and only makes images visible when lozad.js adds a .loaded class. WordPress-exported images don’t have the class lozad expects, so they stay hidden. The fix is blunt but works: add <style>.entry-content img { opacity: 1; }</style> to layouts/partials/extended_head.html.
Cover images on single posts also failed. The theme’s single.html processes cover images through Hugo’s asset pipeline, which only works for images in assets/. WordPress images are in static/. Override layouts/_default/single.html to use a plain <img> tag instead.
Tables from old posts broke in a non-obvious way. Hugo’s Markdown renderer treats 4-space-indented lines inside HTML blocks as code blocks. Any table HTML with multi-line <tr>/<td> formatting will render as a code block instead of a table. Compact each row to a single line.
Chrome showed a login popup on page load. This turned out to be rel="me" on the Mastodon link in the header. Chrome’s Federated Credential Management API sees that attribute and asks for permission to access other apps. Removing rel="me" from the anchor tag fixed it.
Figure tags inside paragraphs caused another one. When a <figure> sits at the end of a paragraph line without a blank line before it, Hugo closes the paragraph mid-figure and produces broken HTML. The fix is just a blank line before each figure.
Fonts
The Tabor theme uses Lora for body text and Heebo for headings, both from Google Fonts. Add the link tags to layouts/partials/extended_head.html:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;800&family=Lora:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
Deploying
Build with hugo --minify, sync to your server:
hugo --minify && rsync -avz --delete public/ user@yourserver:/var/www/yoursite.com/
This site has been running on WordPress since 2009. Seventeen years of updates, database backups, and the occasional 3am “your site is down” notification. Moving to static hosting doesn’t fix everything, but it removes a whole category of things that can go wrong.