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.
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.
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:
- Fidelity work starts from the extracted DOM of the rendered live page, never from stylesheets alone.
- 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.
- 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.
- 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.
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.
The recipe, written for your robot
The rest of this post is the story of how it went. This section is the process that fell out of it, written so you can hand it to your own agent. Point Claude at this post, tell it which WordPress site is yours, and it should be able to survey the site and come back with a plan. It is welcome to disagree with my choices (some of them are just taste, and at least one is spite). The order of the steps is the part I would hold firm on, since each one exists because I skipped it once and paid for it.
Step zero. Survey the live site
Before any code, somebody has to look at the rendered site and write down what is there. The result is a handoff document. Mine ran to nearly 600 lines for a blog with 25 posts, which is absurd, and it was still the most reused file in the project.
- Count the content through the REST API. Hit
wp-json/wp/v2/posts,pages, andmediawith pagination and record the totals. Mine came back as 25 posts, 10 pages, and 215 media items, which told me the images needed a subset download rather than a full mirror (215 is a lot of duplicate thumbnails). Also confirm the API is actually public before planning around it. The WXR export is the fallback if it is not. - Pull the rendered DOM of one page per template. A post, the home page list, a month archive, a category archive, and a static page. Save the HTML to a gitignored scratch folder and read the body classes. Mine said
right-sidebarand named both a parent and a child theme, which is the fact the whole wrong-site day hinged on. - Screenshot each of those templates with headless Chrome, full page, and save them next to the DOM. Every later fidelity check compares against these. Take them on day one rather than after the owner complains, which is when mine got taken.
- Learn the URL shape from the headers. Curl the apex and the
wwwhost and record which one is canonical, who issues the redirect, and what the permalinks look like. My redirect came from WordPress itself, so it would have vanished at cutover if nobody had noticed. My permalinks were/YYYY/MM/DD/slug/and they had to survive exactly. - Record the hosting reality. Registrar, nameservers, whether a proxy or CDN sits in front, the SSL mode, and any DNS records that would break if proxied. Half of my cutover turned out to be already done because DNS had moved earlier that week, and the survey is where I noticed (pleasant surprise, rare in this project).
- List what the rebuild drops. Every third-party runtime the theme loads. Mine had jQuery, an icon font, a carousel, a preloader, and a breadcrumb plugin. A static blog needs none of them, and writing the list down is how “zero JavaScript” became a decision instead of something that happened.
- Inventory the chrome and the taxonomy. Nav items, sidebar widgets, footer text, categories in use, tagline, and where the favicon assets live. This turns into the component inventory for the theme phase and the redirect map for the category archives.
Questions only the owner can answer
Ask these before proposing anything. If the agent guesses, it will guess something plausible, and plausible is the failure mode this whole post is about.
- Which host is canonical, and does the permalink structure survive or get redirected?
- Which old URLs are allowed to die? Everything not on that list has to 200 or 301 after cutover.
- What gets dropped for good? For me that was comments (spam, entirely spam). The decision went into the rules file so no later session could reopen it.
- Which differences from the old site are upgrades to keep rather than bugs to fix? The old theme broke titles mid-word and the new site was allowed to be better.
- Where do the license lines sit? My theme was GPL and my repo is proprietary, which is the whole reason the clean-room rule exists. Your situation may be different, so ask.
- Where does private end and public begin? Repos, credentials, the hosting account, anything that must never land in a commit.
Write the rules file before the code
Draft CLAUDE.md from the survey and the owner’s answers, then let the owner edit it. It holds four things.
- What the project is, in a paragraph, including what it is not yet. Mine said phase 2 was a framework test and WordPress stayed live, so no session could mistake it for the cutover.
- The standing rules. For me that was the clean room, the prose width cap, and the em dash ban. Yours will be whatever your owner keeps having to repeat (you will know, because you will be repeating it).
- The decisions ledger. Every ruling the owner has made, with the date. Comments dropped, certain URLs allowed to 404, the deliberate divergences from the live site. A fresh session reads the ledger and leaves those alone.
- The failure rules. This section is empty on day one. Every time something goes wrong, the fix becomes a rule here. My four fidelity rules went in after the wrong-site day and the git status rule went in after the wiped renames. It fills up faster than you would like.
The ladder every phase climbs
Each phase, whatever it contains, goes through the same steps. They are cheap, and skipping any of them cost me more than running all of them would have.
- A design spec. A dated document with the scope, the settled decisions, what is explicitly out of scope, how it will be tested, and the exit gate. The exit gate is always the owner reviewing a production preview build. A green test count is not an exit gate (see ninety tests, above).
- A self-review of the spec. Read it back for placeholders, contradictions, and anything that could be read two ways, and fix them inline.
- An implementation plan. The spec cut into small tasks, each with its own tests and its own commit, small enough that a wrong one can be reverted without losing a right one. Planning is also where the spec gets its last correction. My phase 2 plan caught that a bare
/feed/route cannot carry XML on a static host, and the spec was amended before any code existed. - Implementation. Tests first for each task and a commit for each task, with
git statusbefore any subagent gets dispatched. The suite keeps passing throughout. If a task encodes an observation about the live site, one check has to hit the live site. - A writeback. Before the phase closes, anything learned goes into the rules file and the handoff document. This is the only way the next session knows what this one found out, because it certainly will not remember.
- A quality audit. A full read of the source by the main session with no subagents, producing a severity-ranked list where every finding has an ID. Fix each one at the root and write a resolution note under the finding saying what the root actually was. Mine found that two ordering bugs were really one missing module, and that a third finding was latent rather than visible (it was correct by accident, my favorite kind of correct).
- The owner exit gate. Preview build, side-by-side screenshots against the live site, and the owner either signs off or sends it back.
The phases to propose
This is the plan my agent should have proposed on day one and roughly the one it followed once we got there. Yours will differ in the details. I would keep the order.
- Chrome. Recreate the theme from the extracted DOM and measured values. The frame, header, nav, sidebar, and footer get held to a faithful bar. Content components like galleries, tables, and code blocks are allowed to be better than the originals, and the spec says which bar each component is held to. Screenshot against live before calling any of it done.
- The content pipeline. Fetch every published post and page plus the media index through the REST API into a gitignored cache, so conversion re-runs offline and runs are diffable. Convert with a handler table keyed on block class, and make any unrecognized block fail the run with the post slug and the class name rather than passing through silently. Download only the images the converted content references. Emit MDX with frontmatter mapped from the API fields and stamped
generated: true, and refuse to overwrite any file without that stamp. Buffer the writes and flush only on a clean run. Owner content decisions live in a patch table inside the pipeline so they reapply every run. Test the converter with fixture HTML captured from real posts, and test the run with an assertion that every fetched item produced a file. (Yes, that is a lot for one phase. It was also the phase that never went wrong.) - Launch extras. Feed, sitemap, meta and OpenGraph tags, and a real 404 page. These depend on real content, so they wait for the pipeline.
- Redirects and cutover. Build the redirect map from the survey. Recreate the apex redirect somewhere that survives WordPress going away. Then the final pipeline run, then owner review, then the DNS flip, then a crawl of every old URL to confirm each one 200s or 301s. Part 2 covers this end of it.
- Retire the bridge. Delete the pipeline, remove every place it was registered, strip the generated marker from the schema, and commit all of that as one change. From that commit on, the repo is the source of truth.
That is the whole recipe. If your agent comes back with a plan that skips the survey or puts the cutover ahead of an audit, send it back here to read this again.
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.