Loading

SEOSync

← Blog · AI search readiness · Part 27

React SEO checklist for SPAs: Google and AI crawlers

SEO for a React single page app comes down to one rule: every indexable route needs its own URL, its own title and canonical, a correct status code, and its main content in the first HTML response. Googlebot can render JavaScript, so a client rendered SPA can still rank, but most AI crawlers read only raw HTML. This checklist covers the rendering choice, per route metadata, soft 404s, links, sitemaps, hydration, Core Web Vitals, and how to test it all.

Key takeaways
  • Choose prerendering, SSG, or SSR for marketing pages. Keep pure client rendering for the logged in app.
  • The most common SPA bug is a canonical tag pointing every route at the homepage.
  • SPAs return 200 for every URL, including ones that do not exist. Fix soft 404s at the server or host level.
  • Navigation must use real <a href> links, and JSON-LD must be in the initial HTML.
  • Test with View Source and curl. DevTools shows what your browser built, not what bots received.

CSR, SSR, SSG, or prerendering: which should a React site use?

The rendering strategy decides what is in the HTML that crawlers download. Everything else on this checklist gets easier once that is right.

StrategyWhen HTML is builtWhat bots getGood fit
Client side rendering (CSR)In the browser, after JS runsAn empty shellDashboards, logged in apps
PrerenderingAt build time, via headless browserFull HTML per routeExisting SPAs with a few dozen marketing routes
Static site generation (SSG)At build time, by the frameworkFull HTML per routeMarketing sites, blogs, docs
Server side rendering (SSR)On each requestFull, always fresh HTMLLarge catalogs, frequently changing content

A common, sensible architecture for SaaS: the marketing site and blog are SSG or prerendered (Next.js, Astro, React Router framework mode, or a prerender step on your existing Vite or CRA build), and the app behind login stays a CSR SPA, blocked from indexing. You get crawlable pages where it matters without rewriting the product.

What does Googlebot render, and what do AI crawlers fetch?

Googlebot crawls the HTML, queues the page for rendering in an up to date Chromium, and indexes the rendered result. Google describes the process in JavaScript SEO basics. Rendering can lag behind crawling, and it fails if your bundle errors, times out, or depends on APIs that block Googlebot.

AI crawlers such as GPTBot, OAI-SearchBot, ClaudeBot, and PerplexityBot generally do not execute JavaScript. They read whatever text exists in the first response. For a CSR route that is a title and an empty root div, which is why a React site can rank on Google and be absent from AI answers. The full explanation, with examples, is in why AI bots see an empty React page.

The React SPA SEO checklist

1. Main content is in the initial HTML

  • The H1, intro paragraph, and body copy appear in View Source.
  • Pricing tables and plan details are text, not only rendered from an API call.
  • Content in tabs and accordions is in the DOM on load (hidden with CSS is fine), not fetched when a user clicks.
  • No “Loading…” spinner is the only thing in the HTML.

2. Every page has a real, clean URL

  • Use history based routing (/pricing), not hash routing (/#/pricing). Crawlers treat everything after # as the same page.
  • One URL per piece of content. Avoid the same page at several paths.
  • Pick one trailing slash style and one host (www or apex) and redirect the others.
  • Do not put content state that should be indexable into query strings only.

3. Unique title, meta description, and canonical per route

This is where most SPAs fail. index.html ships one title, one description, and often one canonical tag, and every route inherits them. The worst version is a canonical hardcoded to the homepage: it tells Google that /pricing, /features, and every blog post are duplicates of /, so they may never be indexed on their own.

<!-- Bug: hardcoded in public/index.html, served on every route -->
<link rel="canonical" href="https://example.com/" />

<!-- Fix: each route renders its own self-referencing canonical -->
<link rel="canonical" href="https://example.com/pricing" />

React 19 lets components render <title>, <meta>, and <link> tags that React hoists into the head, and libraries like react-helmet-async do the same on older versions. The catch: in a pure CSR app those tags only exist after JavaScript runs. Googlebot will usually pick them up; AI crawlers and link previewers will see the defaults. Prerendering or SSG bakes them into each route’s HTML.

export default function PricingPage() {
  return (
    <>
      <title>Pricing | Acme</title>
      <meta name="description" content="Acme plans from $0. Compare Free, Pro, and Team." />
      <link rel="canonical" href="https://example.com/pricing" />
      <h1>Acme pricing</h1>
      {/* ... */}
    </>
  );
}

Remove the canonical from index.html entirely if you cannot make it route aware. No canonical is far better than a wrong one. Do the same for Open Graph tags so shared links show the right title and image.

4. Real status codes, no soft 404s

SPA hosting usually rewrites every path to index.html with a 200 status. Then React Router renders a “Page not found” component. To Google that is a soft 404: a page that says it is missing but returns success. At scale, it wastes crawl budget and can get junk URLs indexed.

  • Best: the server or host knows your valid routes and returns a real 404 for everything else. With prerendering or SSG, only generated paths exist, and your host serves a 404.html with a 404 status. With SSR, set the status from the route loader.
  • Fallback for pure CSR: Google’s JavaScript guidance suggests either redirecting to a URL that returns a real 404, or adding a noindex robots meta tag in the not found component.
  • Removed products or posts should return 404 or 410, or 301 to the closest replacement.

5. Redirects happen on the server

A <Navigate> or window.location change in React is not a 301. Crawlers that do not run JavaScript never follow it, and even Google treats it as a weaker signal. Put permanent redirects in your host config (for example vercel.json, _redirects, or your CDN rules).

6. Internal links are real anchor tags

Crawlers discover pages by following href attributes. A div with an onClick handler that calls navigate() is invisible to them.

// Bad: no href, crawlers cannot follow it
<div onClick={() => navigate("/pricing")}>Pricing</div>

// Good: renders <a href="/pricing">, still a client-side transition
<Link to="/pricing">Pricing</Link>

Also check that your main navigation and footer links are present in the prerendered HTML, and that important pages are linked from somewhere, not only reachable through search or a dropdown that renders on hover.

7. Sitemap and robots.txt are real static files

  • Generate sitemap.xml at build time with every indexable route and accurate lastmod dates. Leave out app routes, redirects, and 404s.
  • Make sure /robots.txt and /sitemap.xml are excluded from the SPA catch all rewrite. If they return your HTML shell, crawlers ignore them.
  • Reference the sitemap from robots.txt and submit it in Google Search Console and Bing Webmaster Tools.
  • Decide your AI crawler policy deliberately; see robots.txt for AI crawlers.

8. Structured data is in the initial HTML

Google can read JSON-LD injected by JavaScript, but AI crawlers reading raw HTML cannot. Put Organization, SoftwareApplication or Product, BreadcrumbList, and FAQPage markup into the prerendered or server rendered head, and keep it consistent with visible content. For templates, see JSON-LD schema for SaaS.

9. Hydration does not change the content

With SSR, SSG, or prerendering, React hydrates the existing HTML. If the client render differs from the server HTML, React logs hydration errors and may re render the whole tree, causing flicker and layout shift.

  • Do not branch on window, dates, or random values during the first render.
  • Do not hide indexable content behind client only checks such as if (!mounted) return null.
  • Keep personalization (logged in state, geolocation) in small islands that load after the main content.

10. Core Web Vitals and code splitting

Big SPA bundles hurt Core Web Vitals: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS).

  • Split by route with React.lazy and dynamic imports so the pricing page does not download the dashboard.
  • Do not lazy load above the fold content. The hero heading and image should arrive with the HTML.
  • Give images width and height (or an aspect ratio) to prevent layout shift, and use real <img> tags with alt text for meaningful images.
  • Defer analytics, chat widgets, and A/B testing scripts. They are frequent INP offenders.
  • Audit bundle size on each release; it tends to creep up.

11. Crawlers can load your JavaScript and APIs

If you rely on Googlebot rendering anything, it needs the same resources a browser does. Do not disallow /static/, /assets/, or your API paths in robots.txt, and make sure your firewall does not challenge those requests. A page whose data endpoint returns 403 to Googlebot renders as an error state, and that error state is what gets indexed.

12. Social previews and hreflang are per route

Slack, LinkedIn, X, and messaging apps do not run JavaScript when they unfurl a link. They read og:title, og:description, and og:image from the raw head, so these need the same per route treatment as titles. If you serve multiple languages, each localized route needs its own URL and reciprocal hreflang tags in the initial HTML; language switching that only swaps strings in the browser gives crawlers one version.

Common React SEO mistakes

  • Indexable app routes. Login, signup variants, settings, and dashboard URLs end up in the index. Add noindex to app templates and keep them out of the sitemap.
  • Staging leaks. A preview deployment without a noindex header gets crawled and competes with production. Protect previews with auth or an X-Robots-Tag: noindex header.
  • Infinite scroll without pagination. Blog and directory lists that load more items on scroll hide older entries from crawlers. Provide paginated URLs with real links.
  • Prerendering stale data. Build time HTML with last quarter’s prices contradicts what users see after hydration. Rebuild on content changes, or move frequently changing data to SSR.
  • Trusting the Elements panel. It always looks right. Raw HTML is the only source of truth for crawlers.

How do I test a React SPA for SEO?

  1. View Source on each template. Check the title, description, canonical, H1, body text, links, and JSON-LD. Use view-source:, not Inspect.
  2. curl a few routes. Compare titles across routes and confirm a made up URL returns 404.
  3. Run the AI crawler checker. The free AI crawler checker fetches the page as GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot, and Googlebot and compares the words bots read with what a browser shows. A large gap means the route still depends on client rendering.
  4. Use Search Console URL Inspection. View the rendered HTML and screenshot Google produced, and check the canonical Google selected.
  5. Crawl with JavaScript on and off. A desktop crawler such as Screaming Frog can crawl both ways; differences show what depends on rendering. We compare that kind of tool with page level audits in SEOSync vs Screaming Frog.
  6. Measure Core Web Vitals. Use PageSpeed Insights for lab and field data on your key routes.
# Do different routes have different titles?
for p in / /pricing /features /blog; do
  echo "$p: $(curl -s https://example.com$p | grep -o '<title>[^<]*</title>')"
done

# Does a nonsense URL return 404?
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-page-does-not-exist

# Which canonical does /pricing declare?
curl -s https://example.com/pricing | grep -i 'rel="canonical"'

Where should I start if I only have a week?

Do not try to fix every route at once. Focus on the handful of pages that bring in buyers, as described in money pages only:

  1. Remove or fix any hardcoded homepage canonical. This alone can unblock indexing.
  2. Prerender the homepage, pricing, top feature pages, and blog posts.
  3. Give each prerendered route its own title, description, canonical, and OG tags.
  4. Exclude robots.txt and sitemap.xml from the SPA rewrite; generate the sitemap.
  5. Make unknown routes return a real 404.
  6. Move JSON-LD into the prerendered head.
  7. Retest with View Source, curl, and the crawler checker.

Then score the result. A free SEO audit reads the raw HTML and returns an SEO score and an AEO score for the URL, so you can see whether titles, headings, FAQs, and schema now come through, and what to fix next.

FAQ

Is React bad for SEO?
No, but client side rendering is risky. Googlebot renders JavaScript, so a CSR React app can rank, though rendering can lag or fail. Most AI crawlers read only the raw HTML and see an empty shell. Prerendering, static generation, or server rendering for marketing pages gives both Google and AI engines real content.
Can Google index a client side rendered React app?
Usually, yes. Googlebot queues pages for rendering in a recent Chromium and indexes the rendered result. Problems appear when bundles throw errors, time out, rely on blocked APIs, or when every route shares one title and canonical. Use Search Console URL Inspection to see the rendered HTML Google actually produced.
Why does every page in my SPA have the same canonical?
Because the canonical tag is hardcoded in public/index.html, which the host serves for every route. That tells Google all your pages duplicate the homepage. Remove it from index.html and render a self referencing canonical per route, ideally baked into prerendered or server rendered HTML so non rendering crawlers see it too.
How do I return a 404 status in a React single page app?
A catch all rewrite returns 200 for every path, creating soft 404s. With prerendering or static generation, serve only generated paths and let the host return 404.html with a 404 status. With SSR, set the status in the route handler. For pure CSR, Google suggests redirecting to a real 404 URL or adding a noindex meta tag.
Do I need Next.js for React SEO?
Not necessarily. Next.js is one good option, but Astro, React Router framework mode, Gatsby, or a build time prerender step on an existing Vite or CRA app can also produce full HTML per route. Many SaaS teams prerender only the marketing site and blog and keep the logged in app as a client rendered SPA.

Check your React routes the way bots do

Run a free audit on your pricing page to see the SEO and AEO scores of the HTML crawlers actually receive.

Start free View pricing