The usual advice for React SEO is "just use Next.js." That's fine for a new project. If you already have a working Vite or Create React App single-page app, rewriting routing, data fetching, and deployment is a big job. Here's how to get the same result for crawlers without the migration.
A client-side rendered (CSR) React app ships one HTML file for every route:
<head><title>Vite + React</title></head>
<body><div id="root"></div><script type="module" src="/assets/index.js"></script></body>
ReactDOM.createRoot(...).render(<App />) fills that div in the browser. Anything that reads only the raw HTML sees an empty page with a generic title: link preview bots, most AI crawlers, and Google's first pass before rendering. Google renders JavaScript later, but it's deferred, and pages can stay "Discovered" or "Crawled – currently not indexed" for a long time.
curl -s -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" https://www.yourdomain.com/about
Check whether the response has your real text and a page-specific <title>. Then in Google Search Console, use URL Inspection → Test live URL → View tested page to see what Google rendered. Or run the free CrawlFixer scan, which compares raw and rendered output for you.
Even with perfect rendering, SPAs often have weak metadata:
react-helmet-async set them at runtime. That helps Google after rendering, but not crawlers that skip JavaScript, so pair it with one of the rendering options below.sitemap.xml generated from your route list at build time, and a robots.txt that references it. Make sure your host doesn't rewrite these to index.html.<Link to> / <a href>, not onClick navigation, so crawlers can find routes.| Option | How it works | Good for | Trade-offs |
|---|---|---|---|
| Build-time prerendering | A plugin or script loads each route in a headless browser during the build and saves HTML | A small set of static pages | Every route must be known at build time; dynamic data goes stale |
| Self-hosted dynamic rendering | You run headless Chrome (e.g. Puppeteer) and route crawler user agents to it | Teams comfortable running infrastructure | You maintain caching, timeouts, and browser updates |
| Managed edge prerendering | A service in front of your site serves rendered snapshots to crawlers | Any SPA, no code changes | Monthly cost; needs a custom domain |
Google documents dynamic rendering as a workaround and says it isn't cloaking as long as crawlers and users get the same content. Long-term, Google recommends server-side rendering, static rendering, or hydration where practical, so treat prerendering as the pragmatic fix when a rewrite isn't worth it.
CrawlFixer is managed edge prerendering for React SPAs, including apps built with Bolt, Replit, and Lovable (hosted off Lovable). Point www at customers.crawlfixer.com, keep your current host, and crawlers get rendered HTML with titles, descriptions, canonical and social tags filled in, plus a sitemap if you don't have one. Compare it with Prerender.io or Rendertron.
Run the free scan (no signup). If crawlers see an empty root div, try it free for 7 days with no card required. Plans are $12/month or $120/year per site.
Related: Lovable site not indexed by Google · Replit app SEO · Why your Bolt.new site isn't on Google