I had seven Chrome extension games and nowhere good to put them. The two I had already ported to this WordPress site were living inside a base64 blob in the media library, decoded at runtime by a loader — a technique I was quietly proud of and should not have needed at all.
The arcade now runs on Cloudflare Pages. Here is what that actually taught me, including the parts where I was wrong.
The clever technique existed only to defeat my own host
WordPress.com content-sniffs media uploads. A .js file is rejected outright; a .txt file is rejected too once it starts looking like code. Base64 sails through at any size. So the game shipped as a 50,000-character base64 string in a text file, fetched and decoded by a loader script.
That is a real solution to a real constraint. It is also a load-bearing hack: delete the wrong media item and the game dies with no obvious cause. On static hosting the constraint does not exist. ES modules and classic scripts both work natively over HTTP. There is no bundler in this project. The build copies files.
The lesson is not “Cloudflare good, WordPress bad” — this site is still WordPress and will stay that way. It is that I had spent effort engineering around a constraint instead of asking whether the constraint had to apply.
A strict CSP was free, because MV3 already forbade the bad parts
I expected locking down the Content Security Policy to be a fight. It took one file. A source audit across all seven games found zero inline event handlers, zero eval, zero new Function, and zero Workers.
Not because I was disciplined. Because Manifest V3 forbids all of it, so the games were written without those things years before they had a CSP. Every game shipped under script-src 'self' on the first try. Porting an extension to the web inherits a security posture you did not have to argue for.
The one carve-out I needed was connect-src, naming exactly two hosts: Groq, because Veilfall can optionally call it with the player’s own key, and the sync service behind the arcade’s shared play counts. Pinning both means a future dependency cannot quietly add a third.
The _headers file is real, and I did not believe it until I measured
Cloudflare Pages reads a plain-text _headers file from the site root and applies it at the edge. My whole security posture rested on that working, and “the docs say so” is not evidence.
So the test suite serves the site locally with the same policy parsed out of that same file rather than a copy of it, and after deploying I fetched a live game page and asserted the headers came back. They did. A game verified against a permissive local server is a game verified against a server that does not exist.
Two silent failures in Direct Upload
The arcade deploys by uploading a zip rather than from a connected repo. Two things went wrong quietly enough to be worth naming.
The zip root is the site root. Zipping the output folder produces an archive containing a dist/ directory, and Cloudflare faithfully serves your entire site one level too deep. You must zip from inside the folder.
The deploy button is disabled until the upload registers — and clicking it early does nothing at all, with no error. I did exactly that, watched the success page never appear, and only caught it because the deployment list still showed the previous build. Now I check for the “N/N files uploaded” confirmation before clicking anything.
The subdomain is not the interesting part
Pages gives every project a permanent *.pages.dev hostname you cannot opt out of; a custom domain sits on top via a CNAME. That record lives at WordPress.com, which still runs DNS for this domain — so the arcade is Cloudflare-hosted and WordPress-addressed, and both are fine with that.
One thing that looks like breakage and is not: for a while after the DNS lands, the name resolves but TLS fails with a handshake error, because the certificate has not been issued yet. That is the normal in-between state. Do not go editing a DNS record that is already correct.
What I would tell myself a week ago
- Check whether the constraint you are engineering around actually applies to you.
- Verify against production headers, not a convenient local server.
- An extension port arrives pre-hardened. Take the win.
- When a deploy “succeeds”, look at the deployment list, not the button.
The other half of this week — the back button nobody could see, and seven pages with no metadata — is in the arcade write-up. And the reason all seven games needed a home outside the Chrome Web Store in the first place is its own story.

Leave a Reply