React SEO is the work of making a site built with React readable by search engines and AI crawlers, and it mostly comes down to one decision: does each page reach a crawler as finished HTML, or as an empty <div id="root"></div> that only JavaScript fills? React on its own does the second. A React app that renders on the server or at build time does the first and is as indexable as any other site. This guide is for teams running or planning a React site: which pitfalls cost pages, which rendering option to pick, and how to set titles, descriptions and canonicals per route. It is one part of our wider guide to JavaScript SEO and how crawlers read JavaScript sites.
Quick answer
- Plain React renders in the browser. An app started with
createRootsends an empty shell; Google has to render it later, and most AI crawlers and link previews never do. - Render the pages that must rank on the server or at build time. Next.js, React Router in framework mode, or a static site generator all send complete HTML.
- Put the head in the server HTML: one
<title>, meta description, canonical and robots tag per route, with final values. - Use
<a href>links (React Router's<Link>and Next.js's<Link>render them) and real paths, not#/hash routes. - Answer missing routes with a real
404, not a "not found" component served with200. - Check any page with the free JavaScript SEO check, which compares the raw HTML with the rendered page.
Is React bad for SEO?
No. React is a library for building interfaces, and it can produce HTML on a server just as well as in a browser. What hurts SEO is one particular setup: the client-side single-page app, where the server sends the same nearly empty HTML file for every URL and the browser builds the page from a JavaScript bundle.
That was the default for years, because Create React App (CRA) scaffolded exactly this. The React team deprecated Create React App in February 2025 and now recommends starting new apps with a framework. The "Creating a React App" page on react.dev names Next.js (App Router) and React Router in framework mode as its recommended full-stack frameworks. Both render on the server or at build time and can still behave like a single-page app once loaded.
So "React SEO" in 2026 is rarely about React itself. It is about whether your React pages are rendered on the server, at build time or in the browser, and whether the head tags, links and status codes are right in the HTML that crawlers receive first.
Why client-side React loses pages
When a crawler requests a client-rendered React page, the response looks like this:
<!doctype html>
<html lang="en">
<head>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/assets/index-4f2a9c1e.js"></script>
</body>
</html>Google can deal with this, later. It queues the page for rendering in a headless Chromium, runs the bundle and indexes what the scripts produce. The guide on how Google crawls, renders and indexes JavaScript explains the render queue in detail. Four things go wrong on React sites again and again:
- Every page has the same title until rendering. The shell's
<title>is whateverindex.htmlsays, often "React App" or the brand name. Link previews, AI crawlers and Google's first pass all see that. - Content arrives from an API call in
useEffect. If the call is slow, needs a cookie or hits an endpoint blocked in robots.txt, Google indexes the loading spinner. - Navigation with
onClickinstead of links. A<div onClick={() => navigate('/shoes')}>is not a link. Googlebot only follows<a>elements with anhref. - Every URL answers
200. A static host servingindex.htmlfor all paths makes/typo-pagea working, empty page. Google calls these soft 404s.
And beyond Google, most AI crawlers and every link preview (Slack, WhatsApp, LinkedIn) read only the raw HTML. For them a client-rendered React page is the shell above and nothing else.
Which rendering option should you choose?
Pick per page type, not per site. The article on server-side vs client-side rendering compares the options in depth; for React the practical choices are these.
| Option | How React renders | Good for | SEO |
|---|---|---|---|
| Next.js (App Router) | Server Components on the server, or static at build time | Most sites: shops, content, marketing | Complete HTML |
| React Router, framework mode | Server rendering by default; prerender for static paths | Apps that grew from React Router, or teams that want web-standard APIs | Complete HTML |
| Static site generator (Astro with React components, for example) | At build time, JavaScript only where needed | Content sites, docs, blogs | Complete HTML |
| Client-side SPA (Vite, legacy CRA) | In the browser only | Dashboards and tools behind a login | Empty shell |
Next.js is the most common choice and renders on the server by default. Our guide to Next.js SEO covers the Metadata API, sitemaps and the one streaming setting worth knowing about.
React Router in framework mode (the successor to Remix) server-renders by default. Its react-router.config.ts also takes a prerender option: prerender: true renders every static path at build time, and an array or function adds dynamic paths such as /blog/my-post. That lets you host a React Router site as static files without losing HTML per URL.
A client-side SPA is fine where nobody searches: an account area, an admin panel, a web app behind a login. If your public pages and your app share one codebase, move the public pages to a framework first and leave the app client-side.
Note
React Router's ssr: false ("SPA Mode") renders only the root route into index.html at build time. Every URL then gets the same HTML with a loading state, which is better than an empty div but still not content per page. For pages that must rank, list them under prerender or turn server rendering on.
Titles, descriptions and canonicals in React
Every indexable route needs its own head tags in the HTML the server sends. How you set them depends on the framework.
React 19 and later. You can render <title>, <meta> and <link> inside any component, and React places them in the document <head>. The React docs warn to render only one <title> at a time: two components rendering a title both end up in the head, and search engines' behaviour is then undefined.
export default function ProductPage({ product }) {
return (
<>
<title>{`${product.name} | Example Shop`}</title>
<meta name="description" content={product.summary} />
<link rel="canonical" href={`https://example-shop.hr/products/${product.slug}/`} />
<h1>{product.name}</h1>
{/* … */}
</>
);
}On its own, in a client-side app, this still runs only in the browser. The tags reach crawlers in the raw HTML only when the component renders on the server or at build time.
Next.js. Use the Metadata API (export const metadata or generateMetadata) instead of tags in components; it writes the head on the server and deduplicates it across layouts.
React Router framework mode. Export a meta function from each route module, or render the tags in the component on React 19. Both are rendered on the server when SSR or prerendering is on.
Legacy apps with React Helmet. react-helmet-async still works and is server-rendered when the app is. In a client-only app it has the same limit as everything else: the tags exist after JavaScript runs.
Whatever you use, keep the values final. Setting a canonical or a robots tag in the browser after load gives Google two different instructions, and the JavaScript SEO guide explains why a noindex in the shell can keep a page out of the index even when a script later removes it.
Links, routes and status codes
Links. React Router's <Link to="/shoes/"> and Next.js's <Link href="/shoes/"> both render a real <a href>, so Google can follow them. Problems start with custom components: a card that navigates with onClick on a <div>, a menu built from <button> elements, or an <a> without href. Wrap the clickable area in a link instead.
Routes. Use path-based routing (createBrowserRouter, or a framework's file routes), never HashRouter for public pages. Google ignores everything after # when it separates pages, so /#/shoes and /#/shirts are one URL to it.
Status codes. A framework can answer the right code for a missing record: notFound() in Next.js returns a 404 page, and a React Router loader can throw a Response with status 404. In a client-side SPA the server does not know which paths exist, so Google recommends one of two workarounds: redirect with JavaScript to a URL that answers 404, or add <meta name="robots" content="noindex"> to the error view. The guide on SEO for single-page applications covers both.
How to check a React site
Start with your most important page template: a product, category or article page. Compare the HTML the server sends with the page after JavaScript runs.
The check loads the page as raw HTML and again in Chromium, then compares title, description, canonical, robots, headings, links, word count and JSON-LD field by field. On a client-side React page you will see a generic title, zero headings and a handful of words in the raw column. On a server-rendered page the two columns should match, apart from widgets that are fine to build in the browser. The full getReport report runs the same comparison as two findings:
Two quick manual checks help as well. Run curl -s https://example.com/products/blue-shoe/ | grep -i "<title" and see whether the title is the product's. And in Google Search Console, URL Inspection → Test live URL → View tested page shows the HTML Google rendered, with a screenshot.
React SEO best practices
- Server-render or prerender every public page. Main text, headings, images with
alttext, internal links and JSON-LD belong in the first response. - One set of head tags per route, set on the server and never changed after load.
- Fetch indexable data on the server. Keep
useEffectfetching for personal or live data such as a cart, stock updates or recommendations. - Real links and real paths for everything a crawler should reach.
- Real
404and301responses from the server or the framework. - Do not block
/assets/or/api/in robots.txt if the page needs them to render. - Watch the bundle. Hydrating a large bundle delays interaction on mid-range phones; long tasks on the main thread shows how to find the cost.
- Recheck after framework upgrades. A config change such as switching a route to client-only can empty its HTML without any visible difference in a browser.
Common mistakes
- Testing in your own browser only. It runs every script. Compare with the raw HTML.
- Adding a prerendering service instead of fixing rendering. It works, but Google calls dynamic rendering a workaround, and the snapshots go stale.
- Leaving "React App" as the shell title. Even a client-side app should have a sensible default title and description in
index.html. - Hydration mismatches. Rendering different text on the server and in the browser (dates, random IDs,
windowchecks) makes React re-render the tree and can shift the layout. - One
index.htmlwith200for every path. Configure the host to return404for unknown paths, or prerender each real route.
Questions people ask
Is React good for SEO?
Yes, when the pages render on the server or at build time. React itself can produce complete HTML, and frameworks such as Next.js and React Router's framework mode do it by default. The risky setup is a client-only single-page app that sends an empty div for every URL, which Google renders late and most AI crawlers never render at all. Choose server rendering or prerendering for every page that should rank.
Can Google index a client-side React app?
Usually, but later and less reliably. Google queues the page, runs the bundle in a headless Chromium and indexes the result, so content that loads slowly, needs a click or depends on a blocked API can be missed. Titles set only in the browser are missing from Google's first pass, from link previews and from most AI crawlers. Server-rendering the public routes removes the dependency on that second step.
Do I still need React Helmet in React 19?
Often not. Since React 19 you can render <title>, <meta> and <link> in any component and React moves them into the head, which covers what most apps used React Helmet for. Next.js has its own Metadata API. The tags still reach crawlers in the raw HTML only when the component is rendered on the server or at build time, so a client-only app gains nothing for crawlers by switching.
Should I migrate from Create React App for SEO?
Yes, if public pages need to rank. Create React App was deprecated in February 2025 and only builds client-side apps, so every page reaches crawlers as an empty shell. Move the public routes (home, product, category, article pages) to a framework with server rendering or prerendering first; a dashboard behind a login can move later or stay client-side on Vite.