On this page · 9 sections
- 01How does Google index a client-rendered React app?
- 02Which rendering option should a React app use?
- 03Where does a React app's sitemap come from?
- 04How do you connect Search Console and Bing Webmaster Tools to a React app?
- 05How do you send IndexNow from a React app?
- 06How do robots.txt and noindex work in a React SPA?
- 07Which React SPA gotchas keep pages out of the index?
- 08How do you test a React page with URL Inspection?
- 09What is the verify and push checklist for React apps?
Google and Bing can only rank, or use in AI answers, a page they have indexed (added to the library they answer from), so a shell they can't fill stays invisible. This guide shows what a Vite build really serves, the rendering options, sitemaps, Search Console, Bing, IndexNow, soft 404s, and how to test a page, using builds we ran on October 17, 2026.
How does Google index a client-rendered React app?
Google indexes a client-rendered React app in two passes: it crawls the HTML shell, then queues the page for rendering in headless Chromium before it can read the content your JavaScript builds. As of October 2026, Google says it processes JavaScript web apps in "three main phases" (crawling, rendering, indexing), and that a page "may stay on this queue for a few seconds, but it can take longer than that" (Google: JavaScript SEO basics).
This is the complete HTML our default Vite React app (create-vite 9.2.1, Vite 8.3.4, React 19.3.1) served for /pricing after npm run build and vite preview:
<!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-spa</title>
<script type="module" crossorigin src="/assets/index-Cl6Bt99l.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D64VDMd1.css">
</head>
<body>
<div id="root"></div>
</body>
</html>Every route gets this same file: one title for the whole site and an empty root div. Google can still render it, but it adds that "server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript." Bing's webmaster guidelines tell site owners to avoid "Hiding critical content behind client-side rendering" and warn that "Content that cannot be reliably rendered may not be indexed or selected for grounding results" (Bing Webmaster Guidelines). Grounding means the web sources Bing's AI answers, such as Copilot's, draw on.
Which rendering option should a React app use?
The rendering option that helps search most is the one that puts each page's content in the HTML the server sends: prerendering (static generation) for content that changes with deploys, server rendering for content that changes per request.
Option | What crawlers get in the HTML | Good for | Notes |
|---|---|---|---|
Client-side rendering (default Vite SPA) | Empty shell; content after JavaScript runs | Logged-in apps, dashboards | Relies on each crawler rendering |
Prerendering / static generation | Full HTML per route, written at build | Marketing pages, docs, blogs | Rebuild to update |
Server-side rendering | Full HTML per request | Large or fast-changing catalogs | Needs a server or edge runtime |
Dynamic rendering (bot-only HTML) | Full HTML for bots only | Legacy apps | Google calls it "a workaround and not a long-term solution" (Google) |
React's own docs now start new projects with a framework. As of October 2026, react.dev lists Next.js (App Router), React Router (v7), and Expo, plus TanStack Start and RedwoodSDK, and says "All the frameworks on this page support client-side rendering (CSR), single-page apps (SPA), and static-site generation (SSG)" (react.dev: Creating a React App). Create React App was deprecated for new apps on February 14, 2025, with the React team noting that server rendering "also improves performance", not only SEO (React blog).
You don't have to give up a static host to prerender. We scaffolded a React Router app (create-react-router 8.4.0) and set:
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: false, // static hosting, no runtime server
prerender: ["/", "/pricing"], // write real HTML for these URLs at build time
} satisfies Config;npx react-router build printed:
Prerender (html): / -> build/client/index.html
Prerender (html): /pricing -> build/client/pricing/index.html
Prerender (html): SPA Fallback -> build/client/__spa-fallback.htmlServed from a plain static server, curl /pricing/ returned the route's <title>Pricing | Example</title>, its <h1>Pricing</h1> and its body text in the raw HTML. React Router documents the setup in its pre-rendering guide. For Next.js, see our Next.js sitemap and indexing guide.
Where does a React app's sitemap come from?
A client-rendered React app has no sitemap unless you add one: our default Vite build had neither a sitemap nor a robots.txt. The simplest route is a file in public/, which Vite says is "served at root path / during dev, and copied to the root of the dist directory as-is", and which suits files "Never referenced in source code (e.g. robots.txt)" (Vite: static asset handling).
We generate both files before each build with a prebuild script:
// scripts/write-sitemap.mjs (package.json: "prebuild": "node scripts/write-sitemap.mjs")
import { writeFileSync } from 'node:fs'
const SITE = 'https://example.com'
const ROUTES = [
{ path: '/', lastmod: '2026-10-01' },
{ path: '/pricing', lastmod: '2026-10-15' },
{ path: '/blog/launch-notes', lastmod: '2026-10-16' },
]
const urls = ROUTES.map(
(r) => ` <url>\n <loc>${SITE}${r.path}</loc>\n <lastmod>${r.lastmod}</lastmod>\n </url>`
).join('\n')
writeFileSync(
'public/sitemap.xml',
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`
)
writeFileSync('public/robots.txt', `User-agent: *\nAllow: /\n\nSitemap: ${SITE}/sitemap.xml\n`)npm run build copied both to dist/, with dist/sitemap.xml listing the three URLs and dates. Keep ROUTES in step with your router, and use the real date each page changed: Google uses <lastmod> only if it is "consistently and verifiably" accurate (Google: build a sitemap).
How do you connect Search Console and Bing Webmaster Tools to a React app?
Connect Search Console to a React app by verifying with the HTML tag (URL-prefix property) or DNS (Domain property), then submitting /sitemap.xml in the Sitemaps report. Google says the verification tag "must be within the <head> section of the page" and that DNS "is the only way to verify a Domain property" (Search Console Help). In a Vite app, paste the tag into index.html so it's in the HTML the server sends, rather than adding it from a component.
For Bing, import the verified site from Search Console; Bing's help says imported sites "will be added and get automatically verified" (Bing Webmaster Tools help). The alternatives are a BingSiteAuth.xml file in public/ or Bing's msvalidate.01 meta tag in index.html (Microsoft Learn). Our Bing Webmaster Tools guide covers the reports after setup.
How do you send IndexNow from a React app?
Send IndexNow (an open "this page changed" notice that Bing and other participating engines accept; Google doesn't use it) from a React app by putting your key file in public/ and POSTing changed URLs after each deploy. The key file must be served from your host, and one request can carry "up to 10,000 URLs per post" (IndexNow documentation). A static host can't run code on publish, so send it from CI after the deploy step. The sitemap-driven script in our Next.js guide's IndexNow section works for any framework, and the IndexNow explainer covers response codes.
How do robots.txt and noindex work in a React SPA?
robots.txt in a React SPA is a static file in public/, and noindex is a meta tag, which in a client-rendered app is only visible after rendering. Two rules matter:
- Don't block your bundles. Google says it "won't render JavaScript from blocked files or on blocked pages." A
Disallow: /assets/line leaves Google with the empty shell. - Don't rely on JavaScript to remove
noindex. Google warns "When Google encounters thenoindextag, it may skip rendering and JavaScript execution", so anoindexinindex.htmlthat your app later removes can keep every route out. Addingnoindexwith JavaScript to error pages works, because the page wasn't meant to be indexed anyway.
Titles and descriptions can be set from JavaScript: Google says "You can use JavaScript to set or change the meta description as well as the <title> element" (Google). React 19 handles the title natively: render <title> in any component and "React will always place the corresponding DOM element in the document head", but render only one at a time (react.dev: title). In a client-rendered app that still happens in the browser, so the per-page title exists only in the rendered HTML. For crawl rules in general, our WordPress robots.txt guide explains why blocking a URL doesn't remove it from Google.
Which React SPA gotchas keep pages out of the index?
The React SPA gotchas that most often keep pages out are soft 404s, fragment URLs and canonical tags changed by JavaScript.
Every URL returns 200
A single-page app answers every path with the same shell and a 200 status. Vite's default appType: 'spa' includes an SPA fallback (Vite config); in our test, vite preview returned 200 for /pricing and for /no-such-page. Google may then report the missing page as a soft 404. Google's two fixes for client-side routing: "Use a JavaScript redirect to a URL for which the server responds with a 404 HTTP status code (for example /not-found)", or "Add a <meta name="robots" content="noindex"> to error pages using JavaScript."
Hash routes
Routes like example.com/#/products look like one page to Google. Its guidance: "don't use fragments to load different page content", use the History API, and make links real <a> elements with an href, since Google "can only discover your links if they are <a> HTML elements with an href attribute."
Canonical tags set twice
If you inject rel="canonical" with JavaScript, Google says "make sure that this is the only rel="canonical" link tag on the page", and not to change a canonical that was already in the original HTML.
Tools that don't render
Some bots don't run JavaScript at all. The Next.js docs name facebookexternalhit, Facebook's link-preview crawler, as one of these "HTML-limited bots" (Next.js: generateMetadata), and many SEO audit crawlers behave the same way. They see the empty shell, which is another reason prerendering pays off beyond Google.
Rendered pages that Google fetched but still left out tend to show as crawled, currently not indexed, which is about the page's value, not rendering. If you're not sure which of your routes Google has indexed at all, check them free.
How do you test a React page with URL Inspection?
Test a React page with the URL Inspection tool in Search Console, using a live test so you see what Google's renderer produced. Google's steps, as of October 2026: click Test live URL, then View tested page, then the Screenshot tab; it notes "A screenshot of the rendered page is available only in a live test" (Search Console Help: URL Inspection). The same panel shows the rendered HTML, so search it for a sentence from the page body. If the text is there, rendering works; if the screenshot is blank, check the JavaScript console output in that panel and any blocked resources. Google's JavaScript troubleshooting guide also suggests the Rich Results Test for the same check.
What is the verify and push checklist for React apps?
- Fetch a route with
curl. If the body is only<div id="root"></div>, decide whether that route should be prerendered. - Request a URL that doesn't exist. If it returns 200, add the redirect or
noindexhandling above. - Open
/robots.txtand/sitemap.xmlon production. Check the bundle path isn't blocked and every public route is listed with a reallastmod. - Run a live URL Inspection test on one new route and read the rendered HTML.
- Submit the sitemap in Search Console and Bing Webmaster Tools, and send changed URLs through IndexNow.
- Check what Google has indexed a few days later, then fix or push what's missing.
FreshlyCited handles step 6: it checks each URL against Google ("Seen by Google" or "Invisible"), pushes only the pages Google hasn't indexed, and re-checks on days 1, 3 and 7. For sites you verify with an IndexNow key (our IndexNow site verification docs explain how), changes also go to Bing ("Sent to Bing"). To work from your sitemap, use the unindexed sitemap URLs recipe; coding-agent users can add our AGENTS.md instructions. Building with Lovable? Its stack has its own rules, covered in our Lovable SEO guide. Other stacks are in the platform guides.
Shipped new routes? Check your React pages free: 10 free links to try, then 100 free checks a month, no card needed.

