Pre-Rendering a React SPA for SEO Without a Server
React single-page applications render content client-side. Search engine crawlers have improved at executing JavaScript, but they still favor server-rendered HTML. The traditional solutions — Next.js, Remix, or a dedicated SSR server — add complexity and infrastructure. There is a simpler way.
The Problem
When Googlebot visits a client-rendered SPA, it sees an empty <div id="root"></div>. It can execute JavaScript and eventually see the content, but this introduces delays, rendering budget limits, and uncertainty. Social media crawlers (Facebook, Twitter, LinkedIn) do not execute JavaScript at all — they see nothing.
Our Approach: Build-Time Pre-Rendering
Instead of running a server, we pre-render at build time using Playwright. The process is straightforward:
- Build the Vite SPA normally (produces static assets + an empty index.html).
- Spin up a local static server pointing at the build output.
- For each route, launch a headless Chromium page, navigate to the URL, wait for React to hydrate.
- Extract the rendered HTML from the DOM, including all SEO meta tags set by our SEOHead component.
- Write each route's HTML to its own directory (e.g.,
/blog/index.html,/blog/my-post/index.html).
Key Implementation Details
A few things we learned building this for thehtml.studio:
- Collect all outputs before writing. The homepage pre-render overwrites
dist/index.html— if you write it mid-loop, subsequent routes get the pre-rendered homepage instead of the SPA shell. - Use domcontentloaded + a fixed wait instead of
networkidle. Some pages have animations or lazy-loaded assets that preventnetworkidlefrom resolving. - Extract meta tags from the live DOM. Our SEOHead component manipulates the DOM directly (setting title, meta description, OG tags). The pre-render script reads the final state of these tags after React has finished rendering.
Deployment
The pre-rendered output is plain HTML + static assets. Deploy to any static host: S3 + CloudFront, Netlify, Vercel, or a simple Nginx server. No Node.js runtime needed in production.
For CloudFront, we use a CloudFront Function to rewrite clean URLs — /blog becomes /blog/index.html — so the pre-rendered HTML is served directly from the edge.
The Result
Full SPA interactivity for users. Complete, crawlable HTML for search engines. No server to maintain. Build times under 30 seconds for 10+ routes. And all four PageSpeed categories at 100.