---
title: "Is React SEO friendly? React SPAs, Google and AI bots"
description: "React is fine for SEO; an empty SPA shell is not. Google renders JavaScript in a queue, the crawlers behind ChatGPT and Claude read only raw HTML. Test, then fix."
url: https://katman.pro/react-spa-seo-ai-crawlers
lang: en
updated: 2026-10-17
---

Guide Updated 17 Oct 2026

# 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):

```html
<!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”](https://react.dev/learn/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](https://developers.google.com/search/docs/crawling-indexing/javascript/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”](https://vercel.com/blog/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?](https://katman.pro/do-ai-crawlers-render-javascript).

## Test what a bot sees

1.  **Fetch the raw HTML as a bot.** curl never runs JavaScript, so it shows what a non-rendering crawler gets:

```bash
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
```

2.  **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.

3.  **Count both sides.** Headless Chrome prints the rendered DOM, so the gap is easy to measure:


```bash
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):

```text
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](https://docs.lovable.dev/features/seo-aeo), 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](https://katman.pro/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](https://katman.pro/nextjs-seo-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](https://katman.pro/astro-seo-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](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering), 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](https://katman.pro/vibe-coded-site-invisible-to-chatgpt); run those after the rendering fix, not before.

## A prompt for your coding tool (example)

```text
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](https://katman.pro/pricing), [what the MCP does](https://katman.pro/mcp)). 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](https://katman.pro/tools/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.

## Related pages

-   [**How do I make a Vite + React app readable by Google and AI bots?** Vite builds a client-only SPA by default. Four ways to prerender a Vite + React app, with config verified on 15 Oct 2026, plus the hosting fallback that undoes it.](https://katman.pro/vite-react-seo)
-   [**Do AI crawlers like GPTBot and ClaudeBot render JavaScript?** In Vercel and MERJ’s Dec 2024 log study, GPTBot, ClaudeBot and PerplexityBot didn’t run JavaScript; Google and Apple did. What’s measured, what’s assumed, a test.](https://katman.pro/do-ai-crawlers-render-javascript)
-   [**How do I set up Next.js for AI search, and where does llms.txt go?** Next.js renders HTML on the server, yet AI crawlers can still get a blank page or a title in the body. Metadata, robots.ts, sitemap.ts and llms.txt in Next 16.](https://katman.pro/nextjs-seo-ai-search)
-   [**Is Astro good for SEO and AI search, and what still needs setting up?** Astro ships plain HTML by default, so AI crawlers can read it. Still on you: the sitemap integration, robots.txt, llms.txt, JSON-LD and client:only islands.](https://katman.pro/astro-seo-ai-search)
-   [**I built my site with Lovable, Bolt or Cursor, and ChatGPT never mentions it** Built it with Lovable, Bolt, v0 or Cursor and ChatGPT never mentions it? Six traps hide vibe-coded sites; each has a one-minute test and a fix for your AI tool.](https://katman.pro/vibe-coded-site-invisible-to-chatgpt)
-   [**AI crawler checker** Free: we load your homepage as GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and 8 other bots, then show which ones your CDN blocks and what robots.txt says.](https://katman.pro/tools/ai-crawler-checker)
-   [**Katman MCP** Katman MCP runs in Cursor, Claude Code and VS Code: it scans your code, audits your live site like AI crawlers do and checks every fix. Katman Pro from $99/month.](https://katman.pro/mcp)

Updated 17 October 2026 · Katman
