Robot Migrates Blog ~ How I Used Claude to Leave WordPress



In the cutover post I said that if I did my job right, you couldn’t tell the blog changed engines. What I glossed over is how much of that job was actually mine. A large portion of the mechanical work, and honestly a decent share of the investigative work, was done by Claude (Anthropic’s coding agent) with me steering. These are my notes on how that went, written for anyone thinking about pointing an AI agent at their own WordPress exit.

Note

This is a method post, not a product review. The migration worked because of the guardrails I put around the agent. The agent itself is not magic, and one of those guardrails only got written down after Claude confidently built the wrong website. I will get to that.

The WordPress lay of the land

If you have never run a WordPress site, here is the quick rundown. WordPress is a PHP application sitting on top of a MySQL database. Your posts live as rows in that database, not as files you can open in an editor (like VSCode). The look comes from a theme, which is somebody else’s PHP and CSS that you configure through an admin panel. Anything fancier than the basics shows up as a plugin. When a reader loads a page, the server builds it on the spot from all of those parts.

That machinery ran this blog for years and it mostly worked. It is also a lot of moving parts for what is functionally a pile of documents. I was paying DreamHost real money for the privilege, keeping up with plugin security patches was a treadmill, and I never did get the theme looking quite the way I wanted without wrestling somebody else’s CSS (the transition post has the full list of grievances). The grievance that matters for this post though is that none of it lives in files an agent can safely mutate.

I know because I tried. Before any of this migration, I ran an experiment pointing Claude at the WordPress site to update some existing posts, and it was a non-starter. That dead end is a big part of why this migration happened.

Why Astro?

I picked Astro for the usual static-site reasons. It is prose-first, it ships no JavaScript unless you ask for it, and my posts become Markdown files instead of rows in a database. And after that failed experiment, I wanted my content somewhere an agent could actually work on it.

An agent works with file tools. It reads files, greps files, edits files, diffs files. That is the problem with everything in WordPress. There is no grep for a database row or a clicked setting. Everything is a GUI operation with your login credentials, ssh’ing into the box for unsafe manual edits, or cobbling together MCPs (Model Context Protocol servers) to expose an API. Kind of scary from a permissions and security perspective for a live site. An Astro site is the opposite. Every post is an MDX file (Markdown that can embed components), the styling is plain CSS, and the whole site lives in my git repo. When Claude changed something, git diff showed me (and Claude) exactly what changed, and git restore undid it when I disagreed. No prompt I wrote mattered as much as being able to do that.

Two other properties were a nice benefit. The build is deterministic, meaning the same input files produce the same output every time, so “re-run it and diff” becomes a legitimate way to verify things. And Astro’s content collections let you type your frontmatter with a Zod schema (a validation library where you declare which fields are allowed and what type each one holds). We eventually made both schemas strict, so an undeclared frontmatter key fails the build and tells you the file and the key. This matters more with an agent than without one. An agent’s natural failure mode is the plausible-looking mistake, and I was reviewing its work, not writing my own. If a mistake does not break the build in my face, there is a real chance I scroll right past it.

The technical path from WordPress to Astro is nothing exotic. WordPress has a REST API, so every post and page got pulled from that and cached as local JSON. A converter then turned the Gutenberg block HTML (Gutenberg being the WordPress editor) into MDX and mapped the API fields into each file’s frontmatter. The interesting decisions are in how that pipeline gets run, and I will cover that below.

Write the rules down before the robot starts!

Claude has no memory between sessions. Whatever you told it on Tuesday is gone on Wednesday unless it is written in a file the agent reads every time. So the first real thing this migration produced was a CLAUDE.md in the repo, a file that says what the project is, what the standing decisions are, and which mistakes have already been made once. The code came later.

The most important rule was the clean-room rule. WordPress themes are GPL licensed. I could legally copy the theme’s CSS into my repo, but that would drag a copyleft obligation into a repo I want to keep proprietary. So no theme file ever gets copied in, period. The look gets rebuilt from measurements taken off the rendered live site (this color is #404040, that border is 2px, the container is 1380px), and any reference CSS fetched for measuring sits in a gitignored scratch folder that never gets committed. A human contractor hears a rule like that once and remembers it (well hopefully). With an agent, if it is not in the file, the next session starts without it.

The other thing the rules file holds is decisions. Early on, Claude kept “fixing” things I had chosen on purpose, because a fresh session had no way of knowing what my taste was. It happened more than once before I caught the pattern. So now the rulings get written down. Some divergences from the old site are deliberate. Certain URLs are allowed to 404. The prose column never gets width-capped. The next session reads that list and leaves my choices alone.

A bridge, not a pile of hand edits

The tempting way to migrate a blog’s worth of posts is to have the agent convert them once, then fix problems by hand-editing the output files. I want to talk you out of that, because avoiding it was probably the best engineering decision of this project.

The migration was built as a re-runnable pipeline [fetch, cache, convert, emit]. When a converted post had a problem (a mangled table, a decorative separator in the wrong spot, an image caption gone missing), the fix went into the converter rather than the output file. Then the whole pipeline re-ran from the cached snapshot and rewrote every post on the site. Determinism made this safe, since a re-run after a pure refactor produced a zero-line diff, which is basically a regression test (for free no less). Every content fix accumulates in one place and survives every re-run.

The pipeline also protected me from itself. The emitter stamped every file it wrote with generated: true in the frontmatter and refused to overwrite any file without that marker. The day I started hand-editing a post, the marker came off, and the pipeline could no longer wipe out my writing. Overkill? Maybe. But a fresh session does not know the migration is destructive, and I did not want to learn what happens when one re-runs it over my edits.

At the end of the migration, the pipeline got deleted. Wiped out so an agent wouldn’t accidentally start calling the tooling against a dead WordPress site (or now against the Pages site itself). Since everything was tracked in git I can go back and recover the source, but it’s out of immediate context and thus out of mind. It is weird to actually finish something though and then burn the bridge with a commit.

The day it built the wrong site

This failure taught me more than anything else in the project.

Phase 1 was recreating the theme. Claude fetched the theme’s stylesheets, transcribed the values into a spec, built components from the spec, wrote tests against the components, and reviewed its own diffs against the spec. Ninety tests were green (which in a pre-Claude time would have been maybe one script test and certainly not structured into a test suite for future use). The header was wrong, the sidebar was wrong, and the footer contained copy that has never appeared on any website I own.

The root cause is almost funny in hindsight. A WordPress theme ships far more CSS than any one site renders. Mine is a parent/child pair where the parent stylesheet is 91 KB covering every configuration the theme supports, and the live site uses one particular slice of it, selected by the child theme and the customizer (again remember WordPress is a complicated machine that is trying to do a lot more behind the scenes). So working from the stylesheets alone, Claude hallucinated a theme configuration that does not exist, then faithfully built it. And every layer of checking downstream inherited the mistake, because the tests verified the components against the spec, and the spec was wrong (maybe I could have been more careful but on read it seemed correct; perhaps there is a lesson in trying to define features in underspecified human language somewhere here). Nothing in the chain ever looked at half-built-robots.com. Ninety green tests! Every one of them agreeing with a site that never existed.

The fix turned into standing rules:

  1. Fidelity work starts from the extracted DOM of the rendered live page, never from stylesheets alone.
  2. A side-by-side screenshot of ours versus live is part of the definition of done for any visual change. Headless Chrome was sitting right there the whole time; the first screenshot only happened after I complained.
  3. Claude reviews production preview builds, never just the dev server. I review both. Hot reload kept serving stale styles, so fixes that had already landed looked broken, and I caught myself distrusting work that was actually fine.
  4. When a spec is describing an external system, at least one verification step has to check the actual system and not the spec.

That last one is the general rule. An agent will happily build you a self-consistent world, and somebody has to keep checking it against the real one. That somebody is you.

Trust, but git status

One more lesson learned from this project. Partway through I had a set of uncommitted renames sitting in the working tree when I dispatched a subagent to do something unrelated. The subagent did exactly what it was told, had no idea my changes existed, and wiped them.

I was mad at the machine and perturbed at myself for a lack of discipline, dispatching an agent while my uncommitted changes were just sitting there. The fix is simple enough. I run git status, the agent runs git status, everyone is always committing frequently. Git can restore anything committed. The uncommitted stuff is just gone. Get your edits into git please. Respinning the LLM generation lottery on a spec is time spent rehashing what was already built but only you remember. It’s really frustrating.

So who did what?

Claude did the bulk of the work. It built the pipeline, converted every post with images, captions, and internal links intact, generated the redirect map, wrote a couple hundred tests, and ran the audits where it read the whole repo and ranked its own defects. I made the judgment calls. Which old URLs were allowed to die, what the categories should be, and which differences from the old site got kept because they were simply better. (The old theme word-breaks titles mid-word. The new site wraps at word boundaries.)

I also did a lot of verifying, looking at preview builds and screenshots against the live site. That part I could not delegate. Everything else in this post is really about making that one job manageable.

Was it faster than doing it myself? Once the guardrails were in place, dramatically so. Before that, I paid the difference back in rework from the fidelity failure above. That is also why the guardrails got most of the word count in this post.

Part 2 covers the other end of the move, getting the Astro build onto Cloudflare Pages and flipping DNS. That turned out to be the short, pleasant half of the project.