A few months ago I moved a batch of WordPress sites off a shared LAMP host and onto Cloudflare Pages as static exports.
The pitch is obvious: no PHP process to keep patched, no MySQL to babysit, effectively free hosting, and a CDN in front of everything by default.
What the pitch doesn't tell you is how many small, boring things break on the way there.
This post is a rundown of what actually went wrong migrating a set of ten WordPress sites — one of them is burningtribe.tokyo, which I'll use as the concrete example — and how I fixed each issue.
The approach The migration itself is conceptually simple: crawl the live WordPress site, save every URL as a static HTML file plus its assets, and serve that tree from Cloudflare Pages.
I used a combination of and a custom crawler for a couple of sites where choked on query-string-based pagination.
The static output then gets pushed with .
No build step, no framework, just files.
That simplicity is exactly why it seemed low-risk.
It was not.
Problem 1: relative canonical tags pointed everything at the homepage The first thing I noticed after deploying was that Google Search Console started reporting most inner pages as "duplicate, Google chose different canonical" — and the canonical it picked was the homepage.
The cause was almost funny once I found it: the WordPress theme emitted as a relative path in a few cached page fragments, instead of an absolute URL like .
On the original WordPress install this didn't matter because the page itself resolved the relative reference correctly at the point of caching.
Once the HTML was frozen and served statically from a different origin structure (Pages serves everything from the apex), that relative canonical collapsed to the site root for every single page that had it.
The fix was a straightforward but tedious pass: grep every exported HTML file for , and rewrite any relative or root-relative href into a fully qualified absolute URL matching that page's own path.
I wrote a small script that walked the export directory, parsed the canonical tag, and replaced it based on the file's own location relative to the site root.
Running that across all ten sites turned up the same bug in three of them — it wasn't a burningtribe.tokyo-specific quirk, it was a plugin combination that a few sites shared.
Problem 2: no sitemap.xml survived the export WordPress sitemap plugins (I was using a common SEO plugin's built-in XML sitemap) generate their sitemap dynamically from a PHP endpoint, not as a static file anywhere in the theme or uploads directory.
A mirror crawl that only follows links will never discover unless it's explicitly linked from a page, which it usually isn't — it's referenced from and submitted directly to Search Console instead.
The result: after cutover, sitemap.xml on the new static site was either missing entirely or, worse, still referencing the old dynamic URLs that no longer existed as generated endpoints.
For a couple of sites this went unnoticed for almost two weeks because the sites still ranked on already-indexed pages; the failure mode is silent.
I ended up regenerating sitemaps after the fact by walking the exported file tree directly (every file becomes a entry) rather than trying to preserve the plugin's output, since the plugin's output no longer had anything running to produce it.
That also meant re-submitting fresh sitemaps in Search Console for each property, which is worth doing explicitly rather than assuming the crawler will pick up the change organically.
Problem 3: enqueued JS and CSS 404s WordPress uses and to load most theme and plugin assets, and those functions frequently append a version query string, like .
A naive mirror tool that treats and as different resources will save one and reference the other, or save the query-string version and leave the extensionless base path unresolved depending on crawl order.
I saw a batch of these appear as 404s in the Pages deployment logs for image lazy-load scripts and one anal