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.
#The three build steps
"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 exportsrender(path)using React Router's static router andrenderToString. - 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.
#Related notes
Sources
- 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 questions
Building something in this space and want another pair of hands on it?
Get in touch