Robot Ships Blog ~ Astro to Cloudflare Pages (and How I Ditched DreamHost)



Part 1 was the long road out of WordPress. This is the short road onto Cloudflare Pages, and it is short mostly because Pages doesn’t leave you much to do. You connect the repo, tell it the build command, and get out of the way. The interesting parts are a redirect that would have quietly died at cutover, and the rehearsal that let me flip DNS without holding my breath (I still did but I’m config change squeamish).

Claude’s job in this half was smaller. It wrote the cutover runbook and the checklist, and every verification step in the checklist is a curl block that can be pasted and re-run. I asked Claude to run validations after sections to make sure everything was going smoothly.

Wiring the build

The setup is all dashboard clicking. In Cloudflare, under Workers & Pages, I created a Pages project and connected the GitHub repo. Framework preset Astro, build command npm run build, output directory dist, and one environment variable (NODE_VERSION set to 22) so the build machine matches the Node on my desk. The first build came back green with 57 pages, 17 redirect rules parsed from the _redirects file, and a sitemap. The project also gets a *.pages.dev hostname, which ended up being where the entire cutover got rehearsed.

Pages builds from private repos natively, so the repo stays private and no tokens get pasted anywhere. Hosting cost is zero. A static blog fits inside the free tier with room to spare (so long as y’all don’t go crazy).

Branches are environments

One part of Pages reorganized my workflow in a way I did not expect. main deploys to production automatically on push, and branches can get their own public preview URLs. I have preview deployments locked down to dev and preview-* branch names, so a random feature branch doesn’t deploy anywhere. The dev branch’s preview URL is now my staging site. Feature work stays in a tighter local loop, but before anything merges to main I review it on the dev preview, on real hosting with real redirects instead of a dev server. Part 1 has a whole section on why reviewing real builds matters; Pages makes the real build the easy default.

Auto-deploying main also means an absent-minded push goes straight to production. GitHub’s branch protection is not available on a free-plan private repo, so the repo carries a committed pre-push hook that refuses local pushes to main outright. Day-to-day work goes onto dev, and main moves only by a deliberate pull request. A hook is weaker protection than anything server-side, but the accident I am guarding against is me (or an agent) pushing to production without thinking, and it ought to stop that just fine.

Rehearse on pages.dev

Before DNS moved, everything testable got tested against the pages.dev hostname (thank you Claude). The old WordPress URLs are date-based (/2025/03/10/slug/) and the new site preserves those paths. The checklist crawls the old URL list and sorts every result into one of three buckets. Preserved URLs have to return 200. Moved ones (renamed category slugs, the retired /feed/ path) have to 301 to the right target. And the eight URLs I deliberately let die have to 404 on the custom 404 page.

One quirk worth noting. Astro’s local preview server and Pages disagree about un-slashed URLs when trailingSlash is set to "always". The preview server answers /nope with its own terse error page before ever consulting your custom 404, while Pages serves the custom 404 for both /nope and /nope/. So the 404 behavior had to be verified on the actual hosting. The rule from part 1 applies to hosts too. At some point you have to check the actual system and not your model of it.

The redirect that would have died

The apex domain (the bare half-built-robots.com, no www) 301s to www, and it had done that for years, so I assumed it was handled somewhere in DNS. Then curl -sI on the apex showed the header x-redirect-by: WordPress. WordPress itself was issuing that redirect. The moment WordPress went away, the apex would break and every link to the bare domain would die with it (not that I had much search presence or backlinks but it’s the thought that counts).

The fix took a couple of minutes. A Cloudflare redirect rule sends the apex to www with the path and query string carried through, created and verified before the flip. Claude finding that problem is the part that impressed me. I would never have noticed which layer issues a redirect by clicking around, because the redirect works either way. Claude noticed because it habitually reads response headers and apparently does not get bored doing it.

Pull the Lever, Kronk

And now the big switchover, which was actually pretty quick and painless. Simply add www as a custom domain on the Pages project, accept the CNAME change, wait. Propagation took about ninety seconds, and then www was serving the Astro site with no WordPress fingerprints in the HTML.

WordPress was not touched. It is still sitting at DreamHost, fully intact, dreaming its PHP dreams and serving pages to absolutely no one, kept on as the rollback. Undoing the entire migration would have been repointing DNS back to the old origin.

No wrong lever this time.

Afterward

Minutes after the flip, the checklist crawls re-ran against the live domain. Sixteen preserved URLs at 200, nine redirects landing on their targets, eight deliberate 404s, and a sitemap on www. The feed also got some attention. I don’t use RSS myself, so it is hard for me to judge what feed readers want, but a broken feed is something nobody would ever report. It would just quietly stop updating for them. So the legacy /feed/ paths all 301 to /feed.xml, the feed validates, and conditional GET returns 304 so feed readers poll cheaply. For analytics I turned on Cloudflare’s built-in web analytics, which is just a toggle in the dashboard. It does not use cookies, so I do not need a cookie banner either.

And now we soak. The site sits for a couple of weeks with WordPress parked as the rollback while I watch Search Console and analytics for surprise 404s.

Ditching DreamHost

If the soak turns up nothing, the WordPress hosting gets canceled, and the last recurring bill this blog has, besides domain name, goes with it (which sort of offsets the token spend right?). Everything I cared about already made it out and lives in git, so turning off WordPress will not be much of a moment.

Infrastructure simplification achieved.