Contents

Retiring WordPress – How I Migrated fryguy.net to Hugo (With an AI Pair Programmer)

This site has run on WordPress since basically forever. As of this week, it doesn’t anymore. Everything you’re reading is now a static Hugo site, built from Markdown, served by nginx, with no PHP and no database anywhere in the stack. Here’s why I did it, what actually moved, and how I used Claude Code as a pair programmer for most of the heavy lifting.

Why bother

Let’s be honest about how it got this bad: I didn’t keep up with it. I know. Servers don’t rot on their own — you stop patching them, stop upgrading them, and a few years later you’re staring at a stack that’s aged out from under you. The old droplet had quietly rotted underneath a site that still worked fine. Ubuntu 18.04 — EOL since 2023. PHP 7.4 — EOL since 2022, with no apt-upgrade path to anything newer (the PPA I’d have needed was frozen). MySQL 5.7 — also EOL. WordPress itself was current; everything under it wasn’t, and there was no clean patching path left, just a slow accumulation of risk on a box facing the public internet.

The content itself is mostly a blog — 395 posts going back to 2010, plus a handful of pages and, more recently, a set of interactive network tools. None of that needs a database or a PHP interpreter. It needs to render Markdown into HTML. That’s Hugo’s entire job, and it does it with zero runtime attack surface, because there’s no runtime — hugo build runs once, spits out static files, and nothing executes server-side ever again.

What actually moved

  • 395 posts, 22 pages, 705 media attachments — exported from WordPress via the Hugo exporter plugin’s CLI mode (the web UI export kept timing out and filling disk on a full run).
  • A new theme (LoveIt, swapped in partway through after starting with PaperMod) with dark/light mode, built-in client-side search, and no server-rendered anything.
  • The network tools section grew from 8 tools to 22 — VLSM planner, BGP community parser, TOTP generator, IGP metric calculator, a password generator with a Diceware mode using the real EFF wordlist, and more. All client-side JavaScript, all verified against real reference data before shipping (RFC test vectors, textbook subnetting examples, Cisco’s own published EIGRP worked examples) rather than “looks right, ship it.”
  • A real production home: a fresh Ubuntu 24.04 droplet, nginx, Let’s Encrypt/ZeroSSL certs via acme.sh, ufw + fail2ban, and a git pull && hugo build && rsync deploy script. DNS for the real fryguy.net domain cut over this week; the old WordPress droplet is still sitting there, untouched, until I’m confident enough to shut it down for good.

How AI actually helped

I did this whole migration working with Claude Code rather than writing every script and config by hand. A few places where that was a genuinely material difference, not just “AI wrote some code for me”:

Bulk content repair at a scale I wouldn’t have done by hand. The WordPress export wasn’t clean — 2,500+ image URLs still pointing at Jetpack’s Photon CDN instead of local files, internal links hardcoded to the old domain, a handful of content bugs that only showed up once you tried to actually build the site (a stray telnet:// URL that broke Hugo’s link parser, front-matter type mismatches that made the new theme’s homepage template return zero posts). Finding and fixing all of that across hundreds of files is exactly the kind of grinding, error-prone work that’s easy to get wrong by hand and much safer to do with something that can check its own output against the whole site before calling it done.

Rebuilding 22 client-side tools with actual verification, not vibes. Every tool that does real math or crypto got checked against a trusted source before it shipped — the TOTP generator against all five official RFC 6238 test vectors, the EIGRP metric calculator against Cisco’s own published examples, the VLSM planner against a classic textbook subnetting problem. That discipline caught real bugs before they went live rather than after. It also caught one that did ship: a JavaScript syntax error in the password generator (an over-escaped apostrophe) that silently broke all four of its modes, found by adding an actual JS parser to the verification step instead of just eyeballing the diff.

A security audit I actually trust. Before flipping DNS, we ran a real pass across the server (SSH config, firewall, fail2ban, TLS) and the code (a systematic sweep of every tool page for XSS — places where user input or third-party API data gets written into the page without escaping). That caught a genuine bug: a subnet tool’s octet validation used parseInt(), which silently truncates at the first non-digit character, so a crafted value could sneak past validation and get echoed back unescaped. Low severity — it’s a self-inflicted “you’d have to attack your own browser” class of bug — but it’s a real one, and it’s fixed now, along with a few defense-in-depth tweaks elsewhere and a check of Hugo’s own published CVEs against the version I’m running (all patched, as it turns out).

A deploy pipeline bug that would’ve bitten me eventually. Hugo doesn’t delete stale output files on its own — you have to tell it to. My deploy script didn’t, which meant a file I’d deleted from the source kept being served in production for weeks after it was “removed.” Found and fixed, and while backing up the server’s hand-configured pieces (nginx config, the deploy script itself, cron jobs) into the repo, we caught a second copy of the same class of bug: a cron job had the old domain hardcoded as an override argument, which would have quietly undone the DNS-cutover fix the next time it ran.

None of this replaced judgment calls — what to build, what to defer (a Content-Security-Policy header is still on the list, deliberately not rushed), when the site was actually ready to go live, whether a given fix was worth the risk of touching production SSH config. That part stayed exactly where it should: with me. What changed is how much of the mechanical, error-prone, “did I actually check this against a real reference” work got done thoroughly instead of approximately, because there was a second set of hands willing to verify things exhaustively instead of skimming them.

Going forward

I’m going to try to post more here — we’ll see how that goes, I’ve said it before. But the honest version is that my life is slowly shifting away from tech and more toward future planning. Retirement is a lot closer now than my CCIE test was, and that’s a strange thing to sit with. So don’t be surprised if what shows up here starts to drift a bit: still networking and lab stuff when I’ve got it, but probably more of the lessons I’ve picked up along the way, some of what I’ve seen change over a long career, and how I’m thinking about the financial side of what comes next.

If you’re sitting on an old WordPress install that’s accumulated the same kind of EOL rot underneath it, and your content doesn’t actually need PHP or a database, it’s worth asking whether it ever did.