Is React bad for SEO, or is it my single-page app?
React isn’t the problem; a client-rendered single-page app is. Its server sends an empty div, Google fills it in later from a render queue, and the crawlers behind ChatGPT, Claude and Perplexity did not run JavaScript in Vercel and MERJ’s 2024 log study. Test what a bot sees with curl, then prerender or server-render your public routes. App screens behind a login can stay a SPA.
React isn’t the problem, the empty HTML is
React can produce complete HTML on the server. A default single-page app doesn’t. This is the whole index.html a fresh Vite + React project ships (create-vite react-ts template, read 15 Oct 2026):
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Every route returns this shell. Your headings, prices and FAQ appear only after the browser runs the bundle. React’s own docs say build tools “start off with a client-only, single-page app” and that other rendering patterns are yours to add (React, “Build a React app from scratch”, read 15 Oct 2026).
Who reads what from that shell:
| Reader | Runs your JavaScript? | Source |
|---|---|---|
| Googlebot | Yes, from a render queue that can take “longer” than a few seconds | Google, JavaScript SEO basics, updated 4 Mar 2026 |
| Gemini, AI Overviews | Use Google’s index and rendering | Vercel/MERJ log study, Dec 2024; Google AI features docs |
| Applebot | “may render the content of your website within a browser” | Apple support page, 4 Sep 2026 |
| GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, PerplexityBot | Not executed in the study | Vercel, “The rise of the AI crawler”, 17 Dec 2024 |
The last row is a measurement from late 2024, not a vendor promise; the details and what is still assumed are on Do AI crawlers render JavaScript?.
Test what a bot sees
- Fetch the raw HTML as a bot. curl never runs JavaScript, so it shows what a non-rendering crawler gets:
URL=https://yoursite.com/pricing
UA='Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.4; +https://openai.com/searchbot'
curl -s -A "$UA" "$URL" | sed 's/<[^>]*>/ /g' | wc -w
curl -s -A "$UA" "$URL" | grep -o '<h1[^>]*>[^<]*' | head -1
-
Compare view-source with the Elements panel.
view-source:(Ctrl+U or Cmd+Option+U) is what the server sent; DevTools → Elements is the DOM after JavaScript. If your H1 is in Elements but not in view-source, bots that don’t render never see it. -
Count both sides. Headless Chrome prints the rendered DOM, so the gap is easy to measure:
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" # or google-chrome on Linux
echo "raw: $(curl -s "$URL" | sed 's/<[^>]*>/ /g' | wc -w)"
echo "rendered: $("$CHROME" --headless --dump-dom "$URL" 2>/dev/null | sed 's/<[^>]*>/ /g' | wc -w)"
Example: the same wc -w count on the template shell above prints 5, the words of its <title>. On a server-rendered page the two numbers match; our own pricing page, a static Astro build, printed this (own measurement, 15 Oct 2026):
raw: 2451
rendered: 2454
A raw count in single digits next to a rendered count in the hundreds means your text lives in JavaScript. One caution: a spoofed user agent only tests rules that look at the user agent. Hosts that prerender only for verified crawlers (Lovable does this for older React + Vite apps, per its SEO docs, read 15 Oct 2026) will still show curl the plain shell; use Search Console’s live test there.
Your options, from smallest change to largest
| Option | What changes | Good for |
|---|---|---|
| Prerender public routes at build time | Each route gets its own HTML file; the app hydrates as before | Most Vite SPAs; see Vite + React SEO |
| Server rendering per request | A framework renders HTML on each request | Pages with per-request data; see Next.js SEO for AI search |
| Marketing site in a static framework, app stays a SPA | Public pages move to Astro or similar; /app is untouched |
Products where the app is behind a login; see Astro SEO for AI search |
| Prerender service or dynamic rendering | A proxy serves rendered HTML to bots | Last resort; Google calls dynamic rendering “a workaround and not a recommended solution” (Google, updated 10 Dec 2025) |
What else a SPA gets wrong
Rendering is the big one, but three smaller ones travel with it. Client-side routers often answer every unknown URL with a 200 (a soft 404), use onClick handlers instead of <a href> links, and keep one <title> for every route because the per-route title is set in the browser. Each has a one-line test on My vibe-coded site is invisible to ChatGPT; run those after the rendering fix, not before.
A prompt for your coding tool (example)
Our public routes (/, /pricing, /features, /docs/*, /blog/*) must ship their
text in the HTML without JavaScript. Pick the smallest change for our stack
(build-time prerendering first, server rendering only if a route needs
per-request data). Leave /app/* client-rendered. Every route gets its own
<title>, meta description and canonical in the HTML. Unknown URLs return 404.
Accept when: curl -s https://DOMAIN/ROUTE | grep -c '<h1' prints 1 for every
public route, and the raw word count is within 20% of the rendered count.
Where Katman fits, and where it doesn’t
Katman is an MCP server that runs inside Cursor, Claude Code, VS Code and other AI coding tools. katman_scan_code reads your repo and reports whether each page renders on the server or only in the browser; katman_audit_site fetches the live pages the way AI crawlers do and counts what is in the raw HTML; katman_prompt returns the fix prompt with prerender options for your framework, and a later audit checks the fix landed. Katman doesn’t edit your site: your coding tool makes the change. Katman Pro is $99 a month for up to 3 sites, with fix prompts and page briefs uncapped (pricing, what the MCP does). Nobody can promise an assistant will recommend you; readable HTML only makes it possible. To check bot access without any setup, the AI crawler checker is free.
Written by the Katman team, Rnify Limited. React, Vite, Google, Apple, Lovable and Vercel pages read 15 October 2026.
Who this is not for
- React apps that already server-render or prerender their public pages: start at the six traps on our vibe-coded site page instead.
- Internal tools and dashboards behind a login: they should stay out of search and AI answers.
- React Native apps: this page is about websites.
Frequently asked questions
Can Google index a React single-page app at all?
Yes. Googlebot runs JavaScript in headless Chromium after a queue, so a SPA can be indexed. Google still recommends server-side rendering or prerendering, because rendering is delayed and not every bot runs JavaScript.
Does react-helmet fix SEO for a SPA?
Not on its own. It sets the title and meta tags in the browser, after JavaScript runs, so a crawler that reads only the raw HTML still sees the shell’s one title on every route. Prerender the routes and the tags land in the HTML.
Do I need to move to Next.js?
No. Prerendering your public routes at build time keeps your React code and router, and works on any static host. Next.js makes sense when you also need server rendering per request.
Why does my page look fine in Search Console but not to ChatGPT?
Search Console shows the rendered page Googlebot built after running your JavaScript. The crawlers behind ChatGPT read the HTML your server sent, which for a SPA is a nearly empty div.
Is a prerender service like dynamic rendering safe?
It works, but Google calls dynamic rendering a workaround, not a recommended solution. Rendering the same HTML for everyone at build time is simpler to keep correct.
Which pages should I prerender?
Every public page you want found: home, pricing, features, docs, blog and situation pages. Dashboards, settings and anything behind a login can stay client-rendered.