
Last week, 🤖 Cody and I got the replacement website assembled and connected to Cloudflare. It looked like my blog. It had my posts. On the surface it looked good, but it needed a lot of attention to detail.
All the rows are there
The data was there, but if you recall, I am moving the site from WordPress (which uses HTML-based formatting) to Hugo (which uses Markdown-based formatting).
A lot of posts were mangled. There were spots where the theme change made the layout wonky. There were HTML tags exposing themselves in the middle of articles. Add on top of that the ability to simplify verbose HTML tags into simpler-to-maintain, easier-to-read Markdown.
For example: I use a lot of bulleted lists when I write. The Markdown is the obvious choice going forward, but what about all those lists from old posts? I wanted to clean them up, rather than carrying it around as tech debt.
Which one of these would you rather maintain?
HTML?
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>or Markdown?
- Item 1
- Item 2
- Item 3In order to make sure all of my content was kept intact, I needed to make sure the new site was a faithful copy of the old one. I could go page by page, comparing the live website to the new one, but I ain’t got no time for that. Grunt work. Toil. Whatever you call it, I don’t like doing it. But I had a plan.
While WordPress kept serving the real site, the replacement had its own test domain. I had hired Cody at $20/month to help me write code. He could compare the two side-by-side, page-for-page, URL-for-URL.
The themes were different, so we weren’t looking for a pixel-perfect match. Was the overall formatting correct? Were the images rendering? Was broken HTML bearing its tags in front of the reader? A paragraph wearing its HTML tags on the outside needs fixing.
I gave Cody the task of doing the side-by-side review to identify pages that served a broken experience to a user. His job was to bundle up fixes into PRs that had 5–10 fixes each (across 1–10 pages), and queue them up so I could review them.
On my $20/month Codex service tier, when Cody tried to implement my instructions, I ran out of tokens in about 15 minutes. Five hours later, when my quota reset, I told Cody about the token faux pas, and suggested a different workflow to minimize token use.
Token use tangent
We’ll get back to the website, but let’s talk tokens. You are probably asking, “What are tokens? What are they used for?” They are the currency for AI agent usage.
Think about what AWS charges for: ingress, egress, storage, and compute. Pretty standard consumption-based billing in today’s software cloud landscape. Those same concepts are going to drive your token usage.
Cody’s first attempt (based on my guidance) tried to use a powerful model and subagents, taking one post from start to PR before moving on to the next. Conceptually, think about each subagent as spinning up a new VM (the subagent), bootstrapping the needed applications and services (filling its initial context from the main agent) before it can even start to do real work. It’s not free to initialize—we’re paying for compute during the bootstrapping of the VM, and we’re spending tokens when the subagent initializes.
Instead, I coached Cody into a one-robot assembly line—emphasis on one robot, so we could avoid the subagent startup cost.
- First, he reviewed all the posts and made a list in an .md file of everything that was broken.
- Next, he organized those fixes into categories of similar edits.
- After that, he went through posts by type of fix and packaged up draft PRs. This allowed Cody to share context for similar edits. His context has a lot of the same limits as a database’s buffer cache—if you keep pulling in new stuff, you’re going to push some old stuff out. Next time you need that “old stuff” you’ll have to do more expensive stuff to pull it back into memory (or context). For every step, Cody can process category-by-category so that category’s relevant context stays in context—no token penalty for rehydrating context.
- After queueing up all those PRs, Cody could forget about most of that and take on his new mission of adversarial review. If a PR passed his review, he took it out of draft and made it ready for review. If it didn’t, he left notes and tagged it
needs-fix. - He flipped between the “Developer” and “Reviewer” personas, working category by category until one persona ran out of work, then switching until his punch list of broken pages was done.
After that agent performance tuning session, Cody was able to take off, work steadily through everything, and have it all PR’d for my review by morning.
Each PR was tiny—most were easy to read, and I could quickly tell if they were correct. For some, I’d have to click the preview link in the PR. If it was correct, I merged it. If it wasn’t correct, I gave a normal review, then I used the same needs-fix tag on the PR. When I ran out of PRs, I told Cody to review everything—he did the tedious job of re-reviewing all the merged PRs.
Back to Square One
After a PR is merged, Cody goes back to his review checklist to confirm that everything we signed off on fixing is correct. If I mark something as needs-fix, it’s just a cycle of fixes, reviews, and either approvals or re-fixing.
It’s the same cycle you’d go through with any PR—it’s just that the developer is a robot, so my feedback drives a much faster iteration cycle. Like any other developer, he sometimes gets it right on the first try, and sometimes it takes a few tries.
Somebody has that bookmarked
This is important to me. I’ve invested a lot of time to make sure that my content is discoverable. Folks save bookmarks to posts they find helpful. There are inbound links on other websites that should continue to work. If someone is looking at an old Brent Ozar Unlimited Weekly Links email with one of my posts, and they try to click through to my site, I want them to land in the right spot.
Sounds obvious, sure. That said, it’s an often-overlooked task during a blog migration. I lived through a blog migration while working at SentryOne, when the marketing team moved the blog from WordPress to HubSpot—and mangled every link. A few years ago, Microsoft did a huge migration of blogs company-wide with reckless disregard for maintaining inbound links. As a user, it’s frustrating as hell. I don’t want to do that to my users. I want those links to keep working after the move.
That makes URLs part of the migration, too. Keep the old paths where you can, redirect the ones that need to change, and test an old link directly. Clicking around from the new homepage won’t tell you whether somebody else’s ten-year-old bookmark still works.
This is a small site, with a finite number of pages and redirects. I just sent Cody to do a full regression test to brute-force his way through making sure that every URL from my old WordPress site still worked. Even though this was part of my requirement from the start, this check still turned up a bunch of issues that required me to add or repair some of those URLs and redirects.
Keep in mind—at this point, I hadn’t set the new site live—so every (old-school) am2.co/some/other/thing address could be compared to the (new) test.am2.co/some/other/thing to ensure no addresses from the old site would 404 on the new site.
Did we need all those plugins?
WordPress had been doing more than serving pages. Comments, email subscriptions, analytics. Hugo is made for static websites without native support for those things, so copying the posts wasn’t the whole migration.
This is the “Tech Lego” part I promised. I kept historical comments as read-only content in the post body. Then I had Cody do research on replacements. Based on his research, we settled on giscus for a new comment platform; Buttondown for subscriber email; and Cloudflare for new traffic data to replace the stats I used to keep in WordPress Jetpack.
They aren’t the only choices—they were just what worked best for this blog.
- Giscus asks readers to sign in with GitHub, which is a reasonable fit for my audience. Readers of a non-tech blog may be less likely to already have GitHub accounts, so another service may be a better choice.
- Traffic numbers from different analytics services aren’t automatically comparable because “page views” sound simple, but services count them in different ways. For my database homies, think of this like “query cost”: you could use SQL Server’s “Cost” measurement, with its made-up farkles/query-bucks units. Or you could measure by CPU or IO, or some combination.
- Buttondown is the one service that costs money in my entire tech stack. It’s the only thing I pay a subscription fee for. Why would I pay for an email sending service? Because spam management is hard, email addresses are PII, and subscriber management is another complex problem. I want to outsource PII and subscription management to a service that could do it properly. Even if it’s nothing more than a list of email addresses, that data deserves to be handled properly, and that’s worth paying for.
Some things didn’t need replacing at all. I had a plugin I used for a speaking-event calendar, and it’s been years since I spoke at a conference, so no need to carry that forward. There were a few other plugins that I had for various minor bells & whistles that were just not necessary anymore. Thanks for all you’ve done, Past Andy, but those things have served their purpose and can be put out to pasture.
We now had a replacement that preserved the content, kept the old links useful, and had homes for the features I still wanted. The obviously broken posts were fixed. The deeper checks and HTML/Markdown cleanup could keep going after launch—that was part of the plan from the start.
Next week, we’ll move the domain and give Future Andy a website he can keep using.