2002-2026

I started blogging in 2002 as an outlet. As with other early-Internet technologies, the initial entrants were generally cool people doing the same. It was a lot of fun having cross-blog conversations with fine folks like Kiri, Debbie, Window Manager, and (John (and several other former bloggers).

For me, blogging peaked around 2009, when the big social media players started taking root – Twitter (which was akin to shouting in a room) and Facebook. The real shock came when Google Reader was killed. (I tried dabbling with Newsblur and Reeder.) Now there was no reason to visit my site when you could go to Twitter/Facebook/etc.

In the late 2010s, I picked up a ham radio call sign and decided to migrate everything from jimcarson.com to wt8p.com. I blogged irregularly, but mostly about cooking, geocaching, travel, and ham radio, but without any community, hence “Notes to Self.” To be sure, writing things down has been useful for when I needed steps to reconfigure a radio on a new machine.

Meanwhile, Wordpress had become catnip for script-kiddie bots. Every time I posted, the only feedback I’d receive were spamments. Worse, there were constant attempts to try to hack my site, leading me to install a firewall.

Anyway, last year, I decided I should move to a static site so I can drop my hosted Linux server and let the various WordPress subscriptions lapse.

  • Static site plugin (in Wordpress) - didn’t work as expected, plus it still carried the bloat from Wordpress.
  • Jekyll - maybe this works better with a brand new blog or if you’re a software developer, but I never could get it running.
  • Hugo - I ran into a bunch of problems, especially with the converted markdown, but with some help and about three days of pounding on it, I finally got things working.

In case others are in the same boat, here is the approach I used that finally worked. You’ll notice some underlying themes:

  • Markdown had some errors in it. This could easily have been from ~24 years of blogging, including early migrations from Movable Type to Wordpress to Movable type and finally back to WordPress.
  • Some early images were hosted on Flickr, because it was another early-adopting, fun place with other cool kids. Flicker was assimilated, reguritated, then reemerged, but the subscription was not ideal for me. I grabbed my thousands of photos, but never spent the time to update links. Most of those posts became candidates to delete.
  • Some pages had custom html because that was the way to work around things in 2002-2004. Those were often removed.
  • Holy moley was the images directory bloated with various WordPress themes’ resizing, conversion to webp, and backups.

Overview

The goal was to replace a dynamic WordPress site with a fully static site: plain HTML/CSS/JS files. Ideally, I would piggyback it onto my mail service storage (though I later discovered Cloudflare Pages’ no cost tier works). I don’t want to deal with server maintenance, WordPress security updates, or plugin subscriptions.

Stack:

  • Hugo — static site generator (single binary, no runtime dependencies). Download the extended version for your OS from: https://github.com/gohugoio/hugo/releases
  • PaperMod theme — clean, responsive Hugo theme. Install: git clone https://github.com/adityatelange/hugo-PaperMod themes/PaperMod --depth=1
  • Cloudflare Pages — free static hosting with CDN and custom domain support.
  • Wrangler CLI — Cloudflare deployment tool. Install: npm install -g wrangler
  • wordpress-to-hugo-exporter — WordPress plugin. Install via git clone into wp-content/plugins/: https://github.com/SchumacherFM/wordpress-to-hugo-exporter

Phase 1: Prepare and Export WordPress

Prune content first. I deleted ~1000 posts that I no longer wanted or had huge dependencies on images formerly hosted on Flicker. I also deleted comments, trackbacks, etc. Reasoning: it would be much easier to curate.

Clean up derivative images before exporting.

Over the two decades, WordPress generated tens of thousands of resized images from the originals. There was no way I’d move to Cloudflare without trying to clean things up first. I had a python script, cleanup_wp_images.py, go through the uploads folder and remove anything that appeard to be a resized image, converted to webp, or backup.

Run the hugo exporter from the CLI because doing so from the WordPress UI would always hit a time-out:

cd /path/to/wordpress/wp-content/plugins/wordpress-to-hugo-exporter
php8.3 hugo-export-cli.php

I had a few misfires because the process was writing a copy of the export, then trying to “zip” up the result. This effectively had 3x the storate in /tmp/, which ran out at some point.

There was also a problem I’d caused years ago by creating a symbolic link (e.g., ln -s . i, instead of doing a rewrite rule or editing the images. The hugo exporter was following the links, further expanding the space used. Once I realized what was going on, I simply removed those and it finished much faster.

Fun fact, you can watch the size with the “watch” command:

watch -n 5 "ls -lh /tmp/wp-hugo*"

This is a lot like watching paint dry.


Phase 2: Set Up Hugo Locally

  • Download and install the Hugo extended binary; add to PATH.
  • Create a new site: hugo new site mysite
  • Clone PaperMod into themes/PaperMod (see above).
  • Create content/_index.md containing only opening and closing --- lines. Without this file PaperMod renders a blank home page.
  • Configure hugo.toml (see below).

Minimal hugo.toml:

baseURL = "https://yourdomain.com/"
description = "Your site description"
languageCode = "en-us"
title = "Your Site Title"
theme = "PaperMod"
mainsections = ["posts"]
paginate = 10

[permalinks]
  [permalinks.page]
    posts = "/:slug/"

[markup.goldmark.renderer]
  unsafe = true

[params]
  env = "production"
  mainSections = ["post"]  # matches type: post in exported front matter
  defaultTheme = "auto"

Critical gotcha — mainSections vs type field:

PaperMod filters posts by Hugo’s Type field. Exported WordPress posts have type: post (singular) in their front matter. If mainSections is set to ["posts"] (plural), no posts appear on the home page. Set mainSections = ["post"] to match, or remove the type field from all posts.

Add a navigation menu to hugo.toml:

[[menu.main]]
  name = "About"
  url = "/about/"
  weight = 10

[[menu.main]]
  name = "Archives"
  url = "/archives/"
  weight = 20

[[menu.main]]
  name = "Categories"
  url = "/categories/"
  weight = 30

[[menu.main]]
  name = "Tags"
  url = "/tags/"
  weight = 40

Create an Archives page at content/archives.md:

---
title: "Archives"
layout: "archives"
url: "/archives/"
summary: "archives"
---

Phase 3: Clean Up the Export

Copy the export contents into your Hugo site selectively:

  • posts/ goes into content/posts/
  • Standalone pages (about, privacy-policy, etc.) go into content/
  • wp-content/uploads/ goes into static/images/ (rename the path; update all references in content)

Each of these Python scripts solves a problem I ran into next. Run the Python scripts in this order:

  1. cleanup_wp_images.py — on the WordPress server uploads folder, before exporting
  2. cleanup_frontmatter.py — strips junk front matter, rewrites image paths, flags Flickr posts
  3. fix_yaml_lists.py — fixes inline single-item YAML lists
  4. fix_cover.py — if cover images came out flat instead of nested YAML
  5. rewrite_image_refs.py — rewrites derivative image references to base originals
  6. find_unreferenced_images.py — identify and delete unused images
  7. fix_heading_html.py — add blank lines between headings and HTML blocks
  8. convert_wp_html.py — convert Gutenberg block HTML to Markdown
  9. resize_images.py — resize large originals to web-friendly sizes (requires: pip install Pillow)

Phase 4: Test Locally

Start the local server and open http://localhost:1313:

hugo server

Check: home page lists posts, navigation works, images load, URLs match existing structure, dark mode works, mobile layout at ~375px wide. Then do a final build:

hugo

Phase 5: Deploy to Cloudflare Pages

Browser drag-and-drop upload is limited to 1,000 files. Use Wrangler CLI for larger sites.

First deploy:

npm install -g wrangler
wrangler pages deploy public/ --project-name yoursite

Wrangler opens a browser for Cloudflare authentication on first run. Initial upload of thousands of files may take several minutes.

Subsequent deploys — Wrangler uses git to detect changed files. Add public/ to .gitignore and commit source changes before deploying:

echo "public/" >> .gitignore
git add content/ static/ hugo.toml
git commit -m "Update posts"
hugo && wrangler pages deploy public/ --project-name yoursite

If you have uncommitted changes you want to deploy immediately:

wrangler pages deploy public/ --project-name yoursite --commit-dirty=true

Custom domain:

In the Cloudflare dashboard: Workers & Pages → your project → Custom Domains. Add both yourdomain.com and www.yourdomain.com. If DNS is already on Cloudflare, this takes seconds. Add a redirect rule for www → bare domain to match your baseURL. Wrangler deployments from a git branch create Preview deployments; use the ... menu → Promote to Production to make them live.


Issues and Resolutions

Issue Resolution
Plugin missing spyc.php Install via git clone, not zip upload — zip upload missed subdirectories containing required files
Wordpress timeout during web export Use the CLI exporter (hugo-export-cli.php) run from the plugin directory to bypass web server timeouts
Export ran out of disk space in /tmp Run cleanup_wp_images.py against uploads first — removes ~80% of files and dramatically reduces export size
Symbolic links in export bloating archive Run find /tmp/wp-hugo-* -type l -delete before archiving to remove symlinks that tar would otherwise follow
Wrong image directory prefix (misc_2018 etc.) Tar was created from a parent directory. Fix: for dir in misc_*/; do mv "$dir" "${dir#misc_}"; done
Home page blank despite correct config PaperMod filters posts by the Type field. Exported posts have type: post (singular) but mainSections was set to posts (plural). Set mainSections = ["post"].
Gutenberg HTML rendered as code blocks Indented HTML triggers Goldmark code-block detection. Run fix_heading_html.py and convert_wp_html.py to fix.
Wrangler uploading 0 files Wrangler diffs against git. Commit changes first, or pass –commit-dirty=true to force upload.
resize_images.py WinError 32 file locking Pillow held file handles open during rename. Fixed by reading entire file into memory with read_bytes() before processing.
PHP-FPM restart command Command is: sudo systemctl restart php8.x-fpm (version number comes after php, before -fpm — opposite of config path order)
PHP zip extension syntax Modern PHP uses extension=zip (no .so suffix). The plugin instructions referencing extension=zip.so are outdated.

Python Scripts Reference

All scripts support dry-run by default (no changes made). Pass --write or --delete to apply changes. Run with --help for full options. Requires Python 3.9+. resize_images.py additionally requires: pip install Pillow

Script Purpose
cleanup_wp_images.py Removes WordPress derivative images (resized variants like -1200x900.jpg, .webp and .bk. backups), keeping only originals
cleanup_frontmatter.py Strips WordPress/Astra theme metadata from Hugo front matter; converts featured_image to PaperMod cover format; rewrites /wp-content/uploads/ paths to /images/; flags posts with Flickr-hosted images
fix_cover.py One-shot patch to fix malformed cover: image: lines left flat instead of nested YAML
fix_yaml_lists.py Fixes single-item YAML lists exported as inline values (e.g. categories: - rant is invalid; should be multiline)
copy_referenced_images.py Copies only images actually referenced in posts from an extracted tar or source directory into static/images/
find_unreferenced_images.py Scans content for image references and reports (and optionally deletes) images in static/images/ not referenced by any post
rewrite_image_refs.py Rewrites derivative image references in Markdown files to point to base originals (strips -NNNxNNN dimension suffixes from paths)
fix_heading_html.py Adds blank lines between Markdown headings and immediately following HTML tags so Hugo renders them correctly
resize_images.py Resizes images exceeding a pixel dimension or file size threshold to web-friendly sizes using Pillow; keeps .orig backups
convert_wp_html.py Converts WordPress/Gutenberg block HTML patterns (figures, tables, lists, blockquotes, bold/italic) to clean Markdown

Writing New Posts

Create a .md file in content/posts/, write in Markdown, build, and deploy:

hugo && wrangler pages deploy public/ --project-name yoursite

Minimal front matter:

---
title: "Your Post Title"
date: 2026-04-01T12:00:00+00:00
url: /your-post-title/
type: post
categories:
  - CategoryName
tags:
  - tagname
---

Place images in static/images/ and reference them in Markdown as ![alt text](/images/photo.jpg)