I built kitdecoder.com, a set of browser-based dev tools, as a plain JS
single-page app. A few weeks in I fetched my own homepage the way a crawler would
and got back the and an empty
structured data only existed after JavaScript ran, so as far as Google was
concerned the site was blank.
The fix is prerendering: visit every route at build time and save the resulting
HTML. I wrote a small Playwright script that serves the build locally, opens each
of the ~40 routes, waits for the h1 to appear, and writes page.content() to
dist//index.html. It worked on the first local run.
Then I put it in the Cloudflare build command and it failed:
error while loading shared libraries: libatk-1.0...
npx playwright install gets you the browser, but Chromium also needs system
libraries that a minimal build container doesn't ship, and I didn't find a way to
install them there. So I stopped trying to make CI do it. The build, Playwright
step included, runs on my machine, and I deploy the finished dist/ with wrangler:
npm run build && npx wrangler deploy
I lost push-to-deploy. In exchange, the deploy can't break on a library I don't
control.
Two other things bit me along the way:
- The prerender step was executing my AdSense script, so every page load during the build fired a real ad request, and one of the saved pages ended up with an ad iframe baked into the static HTML. I now block Google's ad domains with page.route() before navigating.
- page.content() saved the preview server's address into the asset links (http://127.0.0.1:4173/assets/...), which is broken in production. I rewrite those to relative URLs before writing each file.
If you've prerendered an SPA yourself, did you hit the same things?
Top comments (0)