ReactViteSEOStatic Sites

Prerender a React 19 and Vite Site for SEO, No Next.js

How this site renders every React route to static HTML at build time with Vite SSR, then generates the sitemap, RSS and llms.txt from the same data.

This site is a React 19 and Vite app, yet every page ships as complete HTML. Search engines and AI fetchers that never run JavaScript still see the headline, the body, the FAQ and the structured data. The mechanism is small enough to read in one sitting.

#Why not ship an empty div

A client-rendered app returns a near-empty document and builds the page in the browser. Google can handle that, but its documentation says pages with a 200 status go into a rendering queue and may wait longer than a few seconds. The same page says server-side or pre-rendering is still a great idea and that not all bots can run JavaScript. For a personal site whose job is to be found and quoted, removing that dependency is cheap insurance.

3 stepsClient build, server build, prerender scriptThis repository
0Server processes at runtime, the output is plain filesThis repository

#The three build steps

package.json scripts
"build:client": "vite build",
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist-ssr",
"prerender": "node scripts/prerender.mjs",
"build": "npm run build:client && npm run build:server && npm run prerender"
  • Client build writes the browser bundle and an index.html template to dist/.
  • Server build bundles entry-server.jsx, which exports render(path) using React Router's static router and renderToString.
  • Prerender script imports that bundle, loops over the route list, replaces two placeholders in the template with the head tags and the rendered markup, and writes dist/blogs/my-post/index.html.

#One source of truth

The route list, the head tags, the JSON-LD, the sitemap, the RSS feed and llms.txt all derive from the same post registry. Adding a post is one file; the listing page, the sitemap entry, the feed item and the structured data follow without further edits. That is also why nothing drifts: there is no second place where a title or date is typed.

#Hydration and head tags

In the browser, hydrateRoot attaches to the prerendered markup instead of replacing it. The head is written at build time, and a small client component swaps meta tags on in-app navigation, replacing the old JSON-LD rather than appending a second copy, which Search Console reports as a duplicate FAQPage.

#Hosting details that matter

  • Each route becomes a folder with an index.html, so the host redirects the slashless URL. Use the slashed URL as canonical everywhere.
  • There is no single-page fallback rewrite. Unknown paths return a real 404 through ErrorDocument, because rewriting them to index.html returns 200 and creates soft 404s.
  • The .htaccess is a dotfile. File managers hide it, and forgetting it silently drops your redirects and headers.

#Verify the output

Fetch a post with a plain HTTP client and check that the body text, the canonical tag and the JSON-LD are all in the response. If they are, every crawler sees what you see.

Sources

  1. Google Search Central, JavaScript SEO basicsAccessed 2026-10-28. Rendering queue behavior; server-side or pre-rendering still recommended; not all bots run JavaScript.

Frequently asked questions

Does Google index a client-rendered React app?

Usually yes, but with caveats. Google's documentation says Googlebot queues pages with a 200 status for rendering and that a page can wait in that queue longer than a few seconds. It also says server-side or pre-rendering is still a great idea because it makes sites faster for users and crawlers, and that not all bots can run JavaScript.

Do I need Next.js to get static HTML from React?

No. A Vite project can build a second bundle for the server, import its render function in a Node script, write the resulting HTML into each route folder, and let the browser bundle hydrate it. For a site with a small, known set of routes this is a few dozen lines and has no server runtime, which suits shared hosting.

Why does the canonical URL end with a slash?

Because the host serves each prerendered route as a folder with an index.html and redirects the slashless URL to the slashed one. Using the slashed form everywhere, in canonical tags, the sitemap, the feed and structured data, avoids a redirect on every crawl and avoids split signals between two URLs.

Related notes

SEO, AEO and GEO for a Developer Portfolio: What Works

What I changed on this site to be found by Google and cited by AI assistants: crawler rules, prerendered HTML, llms.txt, answer-first writing and honest structured data.

Read

Celery and Redis for LLM Jobs: Settings That Matter

Which Celery settings decide whether slow LLM tasks run once or twice: late acks, prefetch, time limits and the Redis visibility timeout, with documented defaults.

Read

Building something in this space and want another pair of hands on it?

Get in touch