Goodbye WordPress, Hello Astro: Migrating This Blog Without Breaking a Single URL

This blog just changed engines. After almost five years on WordPress, cloudmigration.blog now runs as a static Astro build — same 44 articles, same URLs, same images, same RSS feed. The heavy lifting was done by Anthropic's Fable 5 in Claude Code, fanning out 132 agents in one "ultracode" run. Here is what moved, what it cost, and why the blog continues.

by Attila Macskasy 6 min read

If you are a returning reader, please look around: everything should feel familiar. The same articles, the same URLs, even the same blue-and-green colors I have had since 2021. And yet you are no longer reading WordPress. This blog is now a fully static Astro 5 build. If you did not notice anything — good. On a blog called cloudmigration.blog, the least I could do was migrate the blog itself without breaking it!

Why I finally left WordPress

Nothing was on fire. WordPress 6.8.5 on Apache, PHP-FPM and MariaDB was serving pages every day, much as an agency set it up for me back in 2021. But under the hood it was a tower of moving parts: Elementor plus Elementor Pro, a child theme, Yoast, a forms plugin, a view counter, a cookie banner, an image optimizer — more than ten plugins, each wanting updates, and the premium ones wanting license renewals. The Elementor Pro license had quietly expired, freezing the page builder in time. The little VM was down to 6 GB free on a 15 GB disk. For a personal blog that publishes a handful of well-researched articles per year, I was paying a permanent maintenance tax: core patching, PHP patching, plugin patching, security hardening, license invoices. My wife has long suspected I spend more evenings patching this blog than writing on it. She was right.

A static site deletes that entire category of work. The articles are Markdown files in git, a CI pipeline builds plain HTML, and nginx serves it. No PHP, no database, no admin login to defend, no plugin CVEs, nothing to renew.

One hard requirement: not a single broken URL

Leaving WordPress is easy. Leaving WordPress without losing five years of accumulated Google search authority is the actual project. My articles live at root-level URLs with a trailing slash — the classic WordPress /%postname%/ shape — and those 44 URLs are the only URLs on this site with real search equity. I declared up front, in capital letters in the migration runbook: redirecting them counts as failure, not mitigation. They had to keep answering byte-identically.

What was preserved, with the exact numbers:

  • All 44 articles at their exact old URLs — including my one odd slug with an underscore, /wordpress_migration_challenge/, which forced a wider slug-validation rule.
  • 456 original images (138.8 MB) still answering at their old /wp-content/uploads/ paths. The full uploads tree was 5,131 files and 464.2 MB; the rest was WordPress-generated noise — 2,187 resized derivatives, 2,187 dead .webp sidecars, caches and orphans. The derivatives now 301 onto their originals via one nginx regex, which matters because hundreds of my image URLs sit in Google’s image index.
  • 12 categories and 61 tags as live pages at the same /category/ and /tag/ URLs.
  • The RSS feed at /feed/ — served, never redirected, because many feed readers silently drop a 301 and I was not going to lose real subscribers to save one nginx line.
  • Sitemaps at Yoast’s exact filenames, hand-rolled, because Search Console has those names registered and no off-the-shelf Astro integration produces them.
  • Every meta description byte-identical to the old excerpts, and the old view counts frozen into each article as its historical baseline.

The final contract is a 701-row file listing every URL and its expected status — the identical set, roughly ninety clean 301s, and roughly fifty deliberate 410s for pages that should never have been indexed. A script curls all of them after every deploy. Pass condition: exit 0. No exceptions, no “known failures” list.

I wrote the runbook. An AI executed it.

Here is the announcement inside the announcement. The migration plan is a 1,497-line runbook whose audience line literally reads: an AI coding session or engineer with repo access and no prior context. And that is exactly how it ran: Anthropic’s Fable 5 model, in Claude Code, executed the whole thing end-to-end — extraction over SFTP with the WordPress box kept strictly read-only, a proper Gutenberg block-walker converter for all 44 posts (not a naive HTML-to-Markdown pass — trust me, there is a difference), the URL surgery, the media import, the nginx legacy rules, the full acceptance test.

The part that genuinely impressed me was the “ultracode” multi-agent mode. For the editorial pass — verify, edit, recheck across all 44 articles — Claude fanned the work out into 132 agents: 8,453,167 tokens, 1,221 tool calls, about 37 minutes of wall-clock time, 382 typo and grammar fixes across 40 posts, with my voice preserved. And the detail I keep telling people about: the recheck stage refuted a claim the edit stage had made about its own work — one article’s section headers had been silently flattened, and the cross-examining agents caught it. That is something a single sequential pass, human or AI, structurally cannot do. I have reviewed my share of migration cutovers in my career; that adversarial verification step is where quality actually comes from.

What it cost (honestly)

It was a long job and it burned a lot of credits. I even managed to launch the 132-agent fleet with my 5-hour usage window already at 100%, so about $70 of overage credits vanished in minutes — a lesson in checking the meter before you press the big button. Billed at raw API rates, this single migration would have exceeded an entire month’s subscription price by itself. But on the Claude Max 20× plan, the whole job came to only about 20% of one week’s quota. For replacing a full CMS while preserving every indexed URL, that is the best money-to-outcome ratio of anything in my lab — and for once, a hobby expense I did not have to explain at the dinner table.

My favorite migration bugs

Every migration produces stories. One article turned out to be 306 KB of database content, of which 293 KB was a single base64-encoded PNG pasted inline into a paragraph — strip it, and a perfectly ordinary article remains. A naive static export would have shipped a correct-looking contact form that POSTs into the void; /contact/ is wired to a real endpoint instead. Three agency demo pages full of Hungarian coffee-shop lorem ipsum had been sitting in Google’s index for five years; they finally got their 410.

And my favorite: I carefully froze the old WordPress view counters as a baseline — then rebuilding my analytics revealed the counter plugin had been inflated roughly 10.8× by bots. My apparent #1 article of all time (16,276 views) is in reality #11, with 207 human reads. Humbling! But I would rather build a content plan on 207 real readers than on 16,000 bots.

Same blog, new engine — stay tuned

The old WordPress VM received a full verified backup — 21,191 files and 75 database tables — and was gracefully powered off. Everything you came here for is still here, faster, with zero third-party subresources (even the YouTube embeds became self-hosted preview posters). And publishing a post is now a Markdown file and a git push, which removes my last excuse for the long silences.

cloudmigration.blog continues on the new engine. More hands-on content on cloud migration, the home lab, and — after this experience — AI-assisted engineering is already in the pipeline. If you are new here, /about/ is a good place to start, and /feed/ will deliver whatever comes next.

Stay tuned!