Skip to content

Web & mobile

Prerendering a React SPA for SEO without switching to Next.js

A Vite SPA serves crawlers an empty div. You can fix that in about a hundred lines without adopting a framework or a server.

6 min readByteWeave Studio

The standard advice for a React site that needs to rank is to move to Next.js. Sometimes that is right. Often it is a framework migration to solve a problem that is really about what bytes the server sends before JavaScript runs.

If your site is a marketing site with a known set of routes and no per-request personalisation, you can prerender it at build time and keep hosting it as static files. We do exactly this for our own site, and the whole mechanism is one build script.

What a crawler actually receives

Fetch your deployed page with curl and read the response. A default Vite build returns a document containing an empty root element and a script tag. Google will usually execute the JavaScript and eventually index the rendered result, but it queues rendering separately and can be slow to come back to it.

Everything else is less forgiving. The LinkedIn and WhatsApp preview scrapers do not run JavaScript at all, so a shared link shows whatever static title and description happen to be in the template. Several LLM crawlers behave the same way. If your content only exists after hydration, for those clients it does not exist.

Prerendering in one build step

The mechanism is small. Build the client as normal. Build a second SSR bundle that exports a render function. Then run a script that imports it, renders every known route to an HTML string, and writes each one into the built template at a placeholder such as an app-html comment.

Write each route as its own index.html inside a matching directory, so /about becomes about/index.html. Static hosts serve that at a clean URL with no redirect, and the client-side router takes over on hydration exactly as before. Routing behaviour after the first paint is unchanged.

Per-route metadata is the point

Prerendering the markup is only half of it. The same script should rewrite the title, description, canonical link and Open Graph tags per route, because a single shared title across every page is a bigger ranking problem than an empty root element.

Keep that metadata in one module the app and the build script both import. When the two drift, you get pages whose visible heading and title tag disagree, which is exactly the sort of thing nobody notices for months.

When this is the wrong answer

This works because the route list is known at build time and every visitor sees the same content. If you have user-specific pages, thousands of routes from a CMS, or content that changes between deploys, you want real server rendering or incremental generation, and a framework will do it better than a script you maintain.

The honest test is whether your content changes more often than you deploy. If it does not, prerendering gives you the crawler behaviour of a server-rendered site while your hosting stays a bucket of files with nothing to operate.

  • React
  • SEO
  • Vite
  • Prerendering
  • Static hosting

Have a problem
worth solving?

Tell us what you're building. We'll help you figure out what's possible — and say so if we're not the right people for it.