React SEO
React renders client-side by default, so crawlers see an empty shell until JavaScript runs. Here's how Google actually processes React apps, which rendering strategy to pick, and how to fix routing, metadata, and the render-timeout duplicate-content trap.
React is not bad for SEO — but client-side rendering by default is. Out of the box (CRA, Vite + React), the server ships an empty shell and the browser builds the page, so crawlers see nothing until JavaScript runs. Google can render React via its Web Rendering Service, but rendering is queued, delayed, and can time out — Gary Illyes has shown render timeouts leaving boilerplate-only pages that get marked as duplicates. AI-crawler rendering contracts vary by provider, so CSR-only content adds a coverage risk. The fix is rendering strategy: SSR or SSG (most easily Next.js or Remix) puts content in the initial HTML, hydrated with hydrateRoot (not createRoot) so server and client output match exactly. Then use History API routing, real <a href> links, correct status codes, and version-appropriate metadata: React 19 hoists <title>/<meta>/<link> natively, otherwise react-helmet-async (never the unmaintained original react-helmet).
TL;DR — React is not bad for SEO — but the way most React apps are built is. By default, React builds the page in the visitor’s browser, so when a search engine first fetches your URL it gets a nearly empty page. Google can usually fill in the blanks by running your JavaScript, but it’s slower and riskier than just handing it finished HTML. The fix is to render your pages on a server or at build time — usually with a framework like Next.js.
Why React is different
Most websites — a WordPress blog, say — send the search engine a complete page:
the server builds the HTML and ships it, headline and all. A standard React app does
the opposite. The server sends an almost-empty shell (basically an empty <div>),
and then JavaScript runs in the browser to build the real page.
That’s great for slick, app-like experiences. It’s a problem for SEO, because the first thing a crawler downloads is that empty shell. Your content isn’t there yet — it only shows up after the JavaScript runs.
Can’t Google just run the JavaScript?
Yes — Google runs a real, up-to-date version of Chrome behind the scenes and can execute your JavaScript to see the finished page. So React content can get indexed. Evidence for this claim Googlebot uses an evergreen Chromium rendering engine and can execute JavaScript. Scope: Google Search; successful execution still depends on accessible resources and application behavior. Confidence: high · Verified: Google: JavaScript SEO basics
But there are catches:
- It’s delayed. Google does the rendering later, in a separate step that’s queued up. So your content can take longer to show up in search.
- It can fail. If your page is slow to load its content, Google’s renderer can give up before the content appears — and index a blank-ish page.
- Other crawlers vary. Bing handles JavaScript less reliably, and AI providers do not publish one shared rendering contract. Any crawler that fetches only the initial HTML will see a default React app as empty.
The simple fix
Get your content into the HTML before it reaches the browser. Two ways:
- Server-side rendering (SSR) — a server builds the full page for each request.
- Static site generation (SSG) — pages are built into finished HTML ahead of time. Evidence for this claim React supports server rendering APIs and can be used by frameworks that generate HTML outside the browser. Scope: React server APIs; build-time generation is a framework/build-system capability rather than a React mode by itself. Confidence: high · Verified: React: Server APIs
The easiest path to either is Next.js, a framework built on React that does this for you. (Remix is another good option.) With SSR or SSG, your React site hands crawlers a complete page — and it’s as search-friendly as any normal website.
A few other things to get right
- Use normal-looking URLs (
/products), not hash URLs (/#/products) — Google can’t reliably index the hash ones. - Make your links real links (
<a href>), not clickable<div>s. - Give each page its own title and description that update when the page changes.
Want the deeper version — how Google’s renderer actually works, the render-timeout trap that creates duplicate pages, the rendering-strategy comparison, and how to test what Google sees? Switch to the Advanced tab.
TL;DR — React’s SEO problem isn’t React — it’s client-side rendering by default. CRA and Vite + React ship an empty shell and build the DOM in the browser, so the raw HTML a crawler fetches has no content. Google can render it via the Web Rendering Service (evergreen Chromium), but rendering is queued separately, can be delayed, and can time out — Gary Illyes has documented render timeouts leaving boilerplate-only pages that then get flagged as duplicates. Bing renders JS less reliably; AI-crawler rendering varies by provider. The fix is rendering strategy: SSR or SSG (most easily Next.js or Remix) puts content in the initial HTML — and if you’re hydrating server-rendered markup, use
hydrateRoot(notcreateRoot) and treat any server/client mismatch as a bug, not a warning to suppress. Then use History API routing (not hash URLs) and real<a href>links. For<head>metadata: React 19 hoists<title>/<meta>/<link>natively; on React 18 or for advanced needs, use react-helmet-async (never the unmaintained original react-helmet). Set correct HTTP status codes. There’s no ranking bonus for SSR — it just makes content reliably indexable. For the general rendering mechanics, see the JavaScript SEO and headless CMS topics.
What actually makes React hard for SEO
React is a component-based JavaScript library, and out of the box — Create React App,
Vite + React — it runs client-side. The server returns a near-empty document
(famously just a <div id="root"></div>) plus a bundle of JavaScript, and the browser
executes that JavaScript to construct the DOM. Contrast that with a server-rendered
page (WordPress, a Rails app), where the full HTML — content, headings, links —
arrives in the very first response.
So the question that decides everything is: what’s in the raw HTML before any JavaScript runs? For a default React app, the answer is “almost nothing.” Right-click → View Source on a CRA app and you’ll see the shell, not the content. That’s exactly what a crawler gets on its first fetch.
To be precise about where the responsibility actually sits: React the library isn’t
CSR-only. React DOM ships client rendering (createRoot), server rendering (streaming
and static APIs), and hydration APIs — the library supports all of it. The empty-shell
problem is a property of the default toolchain (Create React App, Vite + React with
no server), which wires up only the client APIs and nothing that renders to HTML on the
server. Swap the toolchain — Next.js, Remix, or React’s own server-rendering APIs — and
the same library ships full HTML on the first response.
This is the React-specific application of the broader JavaScript SEO problem — go there for the general failure modes (parity, interaction, state, timing). Here I’ll focus on what’s specific to React and how to fix it.
How Google actually processes a React app
Google handles JavaScript in three phases: crawl → render → index. Googlebot fetches the URL, the rendered DOM is built later by the Web Rendering Service (WRS) — an evergreen version of Chromium, the same engine as Chrome — and then the rendered output gets indexed and its links extracted. Evidence for this claim Google processes JavaScript pages through crawling, rendering, and indexing using its Web Rendering Service. Scope: Google Search rendering behavior. Confidence: high · Verified: Google: JavaScript SEO basics
The important nuance is when rendering happens. Rendering is resource-intensive, so it’s queued separately from the initial crawl. Martin Splitt described the flow plainly: “we do an HTTP request, and we get something back … some barebone HTML and all it does is load the JavaScript and run the JavaScript. Then, this HTML … goes into rendering. Rendering runs JavaScript — boom!, a lot of content happens that wasn’t there before.” For a CSR React page, the “boom” is your entire page — none of it exists until that render step runs.
A caveat worth carrying: don’t over-index on the old “two waves of indexing” model. Splitt himself walked it back, calling the wave “an oversimplification.” The practical takeaway isn’t “there’s a formal Wave 2 with defined timing” — it’s that rendering is a distinct, deferrable, fallible step, and CSR puts 100% of your content on the wrong side of it.
Two more facts about the renderer that bite React apps specifically:
- It’s stateless. Googlebot does not retain
localStorage,sessionStorage, or cookies between page loads. Any content or routing that depends on client-side state is invisible to the crawler. - It can give up. The renderer enforces a timeout. If your main content loads slowly — large bundles, waterfalls of API calls — rendering can finish before your content arrives, and Google indexes the incomplete page.
The render-timeout trap (and why it creates duplicates)
This failure mode is rarely explained well, and it’s the most damaging one for React apps. Gary Illyes described it directly: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out … and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.”
Walk through what that means for a CSR React app. Your header, nav, and footer are boilerplate that loads fast. Your actual page content — the part that makes each URL unique — is fetched and rendered by JavaScript, and it loads slowly. Rendering times out. Google is left with header + nav + footer on every URL. Now every page looks identical, and Google marks them as duplicates of each other in Search Console.
Illyes’s own fix is the actionable bit: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first and see if that helps.” But the more durable answer is to not depend on the render step for your main content at all — which means SSR or SSG.
Rendering strategies for React
This is the single most impactful decision. The options, roughly worst to best for SEO:
- CSR (default React). Server sends the shell; browser builds everything. Content is delayed by the render queue and exposed to the timeout. Worst for SEO. Fine for authenticated dashboards you don’t want indexed anyway. Evidence for this claim Client-only React rendering constructs UI in the browser; server rendering APIs produce HTML before browser hydration. Scope: React rendering mechanics; SEO impact depends on what the initial response contains. Confidence: high · Verified: React: hydrateRoot React: Server APIs
- Pre-rendering. Build-time rendering without a full SSR framework — tools like
react-snapor a prerender service crawl your app and save static HTML. Lighter-weight; works for simpler, mostly-static sites. - SSG (Static Site Generation). HTML built once at deploy time and served as static files. Fastest, content always present in the raw HTML. Limited for highly dynamic or per-user content; large sites get slow builds.
- SSR (Server-Side Rendering). The server executes React per request and sends full HTML. Content immediately available to crawlers; always fresh. Costs a Node.js server and slightly higher TTFB.
- Hybrid / ISR (Incremental Static Regeneration). A Next.js feature that regenerates static pages in the background — static speed with periodic freshness.
| Strategy | Content in initial HTML? | SEO risk | Best for |
|---|---|---|---|
| CSR (raw React) | No | Highest | Logged-in dashboards, not-indexed apps |
| Pre-rendering | Yes (build time) | Low | Small, mostly-static sites |
| SSG | Yes (build time) | Lowest | Blogs, docs, marketing |
| SSR | Yes (per request) | Low | Fresh, dynamic content |
| ISR / hybrid | Yes | Low | Content that changes hourly/daily |
And one strategy to skip on new builds: dynamic rendering — detecting the crawler user-agent and serving it a pre-rendered version while users get CSR. Google now calls it “a workaround and not a long-term solution” that “creates additional complexities and resource requirements,” and recommends server-side rendering, static rendering, or hydration instead. (Bing recommended dynamic rendering back in 2018, but that guidance is dated — since 2019 Bingbot renders via Microsoft Edge / Chromium, and SSR/SSG is the right call there too.)
There’s a myth worth killing here: SSR is not a ranking boost. As John Mueller put it, “there are no SEO ranking bonuses for implementing it one way or another” — the different rendering methods are “just different ways of making the content indexable.” SSR’s value is reliable indexability (and often better Core Web Vitals from a faster First Contentful Paint), not a magic ranking lever.
Hydration must match exactly — that’s a bug boundary, not an SEO technique
SSR and SSG both hand the browser HTML that already has your content in it. React then has to attach to that markup on the client, and that’s a different API from a plain client render:
createRootrenders React into a DOM node from scratch — no existing markup expected. Use it for CSR-only apps.hydrateRootattaches React to HTML thatreact-dom/serveralready generated, and expects the client’s first render to produce output identical to what the server sent. If you’re SSR/SSG, you wanthydrateRoot, notcreateRoot— callingcreateRooton server-rendered markup means React discards it and re-renders from scratch, throwing away the exact SEO benefit you set up SSR/SSG to get.
Mismatches between server and client output are a real risk in React apps doing SEO
fixes — a Date.now() in a title, a locale-dependent format, an
if (typeof window !== 'undefined') branch. React’s own docs are blunt about what
happens then: it warns about mismatches in development, but “there are no guarantees
that attribute differences will be patched up in case of mismatches.” The guidance is
to treat mismatches as bugs and fix them — not to suppress the warning and assume
content parity. For SEO specifically: don’t assume your rendered content and metadata
match what the server sent just because the page looks right in the browser. Diff the
server HTML against the post-hydration DOM directly (the View Source vs. Inspect
Element check from the testing section below is the fast version of this) rather than
trusting a clean console.
React Router and URL structure
React Router handles navigation in the browser without server round-trips, which is fine for SEO if configured correctly:
- Use the History API, not hash routing.
BrowserRouterusespushStateand produces clean, crawlable URLs (/products).HashRouterproduces/#/products, and Google cannot reliably resolve hash-based URLs — the old AJAX-crawling scheme that made them work is deprecated. Use the History API. - The server must handle those URLs too. With History API routing, every “page” needs
a real URL the server can respond to — critical for SSR, and necessary so a direct hit
or refresh on
/productsdoesn’t 404. <Link>renders a real anchor. React Router’s<Link>component outputs an<a href>, which is crawlable. Navigation built ononClickhandlers without an anchor is not crawlable — Google only follows real<a href>links.
Managing metadata: react-helmet, react-helmet-async, and React 19’s native tags
Through React 18, React never updated the document <head> on route changes natively —
every route’s <title>, meta description, canonical, and Open Graph / Twitter tags had
to be set by a library. React 19 changed that: components can render <title>,
<meta>, and <link> tags directly, and React hoists them to <head> on its own —
working with client-only apps, streaming SSR, and Server Components alike. React 19.2 is
the current stable release as of mid-2026, so this now applies to any app on a current
React version.
That means the right answer depends on your React version and what you actually need:
- React 19, standalone app, only basic tags. Render
<title>/<meta>/<link>in your components directly — no library needed. - React 19, but you need
htmlAttributes/bodyAttributes, SSRcontextserialization,onChangeClientState,prioritizeSeoTags, ortitleTemplate. Native hoisting doesn’t cover these — usereact-helmet-async. Its own docs call this out directly: without those specific needs, you may not need the package at all on React 19. - React 18 or earlier, standalone app. Native hoisting doesn’t exist yet — use
react-helmet-async. It’s actively maintained (major version 3, and it detects your React version at runtime) and supports SSR. - The original
react-helmet. Don’t use it, on any React version. It’s unmaintained — no release since 2020 — and has known bugs under React 18’s concurrent rendering. - Next.js apps. Use Next’s own Metadata API (the
metadataexport /generateMetadatain the App Router) regardless of React version — don’t bolt on Helmet, and don’t rely on native React tag hoisting either. The framework owns the document in a Next.js app.
One reliability rule carries over from JavaScript SEO generally, and applies no matter which of the above you use: HTML-level metadata beats JS-injected metadata. A canonical tag injected by client-side JavaScript is far less reliable than one present in the server-rendered HTML — which is another argument for SSR/SSG.
AI crawlers make this urgent
The 2026 wrinkle: rendering behavior for GPTBot (OpenAI), ClaudeBot (Anthropic), PerplexityBot, and other agents is provider- and version-specific. A CSR React app ships every crawler the same empty shell the first Googlebot fetch sees, and current provider documentation does not establish one shared render step that will fill it in. As generative engines become a bigger discovery surface, SSR/SSG stops being a Google-only concern: raw HTML maximizes coverage without assuming a universal crawler limitation. (The headless CMS topic covers this AI-crawler reality in more depth.)
Testing what Google actually sees
Don’t trust your browser — your DevTools Inspector shows the rendered DOM (post-JavaScript), which is exactly what a crawler-without-JS does not see. Use the right tools:
- View Source vs. Inspect Element. View Source is the raw HTML (what crawlers get before JS). Inspect Element is the rendered DOM. If content is in Inspect but missing from View Source, it’s JavaScript-dependent.
- URL Inspection Tool (Search Console) — the most authoritative check. Run a live test and look at the rendered HTML, the screenshot, and the page resources / console messages to see what Google actually rendered and what failed to load.
- Rich Results Test — a quick rendered-HTML check without verifying the site.
- Disable JavaScript in DevTools and reload — a fast simulation of a crawler that doesn’t execute JS (and a decent proxy for what AI crawlers see).
- Search Console Coverage report — “Discovered, currently not indexed” can signal a render queue backlog; clusters of duplicate pages can signal the render-timeout boilerplate trap.
- JS-rendering crawlers — Ahrefs Site Audit and Screaming Frog (JS-rendering mode) render pages at scale so you can diff raw vs. rendered across the whole site.
Next.js and Remix (the practical answer)
If SEO matters and you’re on raw CSR React, migrating to a framework that renders on the server is usually the right move. Next.js is purpose-built for this — SSR and SSG out of the box, ISR, the App Router, a built-in Metadata API, automatic code splitting, and image optimization. Remix is the web-standards alternative, built on fetch/Request/Response with SSR by default and a strong progressive-enhancement story. Next.js gets its own deep dive — I’m keeping it brief here on purpose. The point for React SEO is narrower: the framework exists to move your content out of the browser-only render step and into the initial HTML.
React is good for SEO when you treat rendering as an architecture decision rather than an afterthought. Pick SSR or SSG for anything that needs to rank, keep your links and routing honest, manage metadata per route, and let Google’s own tools — not your browser — tell you what actually rendered.
AI summary
A condensed take on the Advanced version:
- React isn’t bad for SEO — CSR by default is. React the library supports client,
server, static, and streaming rendering; it’s the default CRA/Vite toolchain (no
server) that ships an empty
<div id="root">shell and builds the DOM in the browser, so the raw HTML a crawler fetches has no content. - Google can render React via the Web Rendering Service (evergreen Chromium), but rendering is queued separately, can be delayed, and is stateless (no cookies/localStorage/sessionStorage between loads).
- The render-timeout trap: if main content loads slowly, rendering times out and Google indexes a boilerplate-only page. Across many URLs those look identical and get flagged as duplicates (Gary Illyes’s documented failure mode). Fix: load content first — or better, don’t depend on the render step (SSR/SSG).
- Rendering strategies, best→worst for SEO: SSG (lowest risk) ≈ SSR ≈ pre-render > ISR/hybrid > CSR (highest risk). Dynamic rendering is deprecated — Google recommends SSR, static rendering, or hydration.
- No ranking bonus for SSR — Mueller: “no SEO ranking bonuses for implementing it one way or another.” It just makes content reliably indexable.
- Hydration is a bug boundary, not a technique: SSR/SSG apps hydrate with
hydrateRoot(notcreateRoot), which expects the client’s first render to match the server’s exactly. React warns on mismatches in dev but doesn’t guarantee patching them — treat mismatches as bugs and verify server vs. post-hydration DOM directly. - React Router: use History API (
BrowserRouter), not hash routing; the server must handle those URLs;<Link>renders crawlable<a href>—onClick-only nav doesn’t. - Metadata: React 19 natively hoists
<title>/<meta>/<link>to<head>for standalone apps needing only the basics. On React 18, or for advanced needs (SSR context,titleTemplate), use react-helmet-async — never the original react-helmet, unmaintained since 2020. In Next.js, use the Metadata API regardless of React version. HTML-level beats JS-injected. - AI-crawler rendering is provider-specific — CSR React depends on client execution that each crawler may or may not support. SSR/SSG puts content in the initial HTML and maximizes coverage.
- Test with View Source vs. Inspect, URL Inspection (rendered HTML + screenshot + console), Rich Results Test, JS-disabled reload, and a JS-rendering crawler.
- Next.js / Remix are the practical fix — they move content into the initial HTML.
Official documentation
Primary-source documentation from the search engines.
- Understand JavaScript SEO Basics — the crawl → render → index pipeline, SPAs, the History API, canonical URLs with JS, and meaningful HTTP status codes.
- Fix Search-Related JavaScript Problems — soft-404 handling in SPAs, the stateless renderer (no cookies/localStorage), and testing with URL Inspection.
- Dynamic Rendering (deprecated workaround) — why Google deprecated it and what to use instead (SSR, static rendering, hydration).
- Introducing a new JavaScript SEO video series — Martin Splitt’s series, covering React, Angular, and Vue specifically.
- In-Depth Guide to How Google Search Works — where rendering sits in crawl → index → serve.
Bing / Microsoft
- The new evergreen Bingbot (Microsoft Edge) — Bingbot rendering JavaScript via the same Chromium platform as Googlebot.
- bingbot Series: JavaScript, Dynamic Rendering, and Cloaking — Bing’s older (2018) take; useful for the historical dynamic-rendering recommendation.
Technical reference
- React v19 (release notes) — native support for rendering
<title>,<meta>, and<link>tags in components, hoisted automatically to<head>. - createRoot / hydrateRoot (React docs) — the client-render vs. hydration API split, and the caveat that mismatches aren’t guaranteed to be patched.
- react-helmet-async (npm) — the maintained fork for managing
<head>in standalone React apps, React 18 or advanced React 19 needs.
Quotes from the source
On-the-record statements from Google’s Search team. Each search-engine deep link jumps to the quoted passage on the source page; the staff quotes below are linked to the coverage that reproduced them.
Google docs — SPAs and dynamic rendering
- “Single-page applications (SPA) are websites that load an HTML document once and fetch any additional content using JavaScript APIs.” — Google Search Central docs. Jump to quote
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” — Google Search Central docs. Jump to quote
Martin Splitt, Google — how JavaScript pages are indexed
- “What we do is we do an HTTP request, and we get something back, right — some HTML, maybe it’s a barebone HTML and all it does is load the JavaScript and run the JavaScript. Then, this HTML that we got from the original HTTP GET request from the crawl, goes into rendering. Rendering runs JavaScript — boom!, a lot of content happens that wasn’t there before.” Read the coverage (Search Engine Journal)
- On the two-waves model being imprecise: “there’s no such thing as the second wave of crawling-ish. The wave is an oversimplification.” Read the coverage (Search Engine Roundtable)
Gary Illyes, Google — the render-timeout duplicate-content trap
- “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.”
- “Do you have a JavaScript-heavy site and you see lots of dups reported in Search Console? Try to restructure the js calls such that the content (including marginal boilerplate) loads first and see if that helps.” Read the post (LinkedIn)
John Mueller, Google — no ranking bonus for rendering choice
- “There are no SEO ranking bonuses for implementing it one way or another.” They’re “just different ways of making the content indexable (as is client side rendering).” Read the coverage (Search Engine Roundtable)
React SEO checklist
A pass to confirm crawlers can see and index your React app:
- Important content appears in View Source (raw HTML), not only in the rendered DOM — if it’s missing, you’re depending on CSR.
- Pages that need to rank use SSR or SSG (Next.js, Remix, or a pre-render step), not raw client-side rendering.
- Main content loads fast and first — no slow API waterfalls that could trip the render timeout into a boilerplate-only page.
- Routing uses the History API (
BrowserRouter), notHashRouter/#/URLs. - The server can respond to every client-side route (no 404 on direct hit or refresh).
- Navigation uses real
<a href>links (React Router<Link>), notonClick-only handlers on<div>/<button>. - Each route sets a unique
<title>, meta description, canonical, and OG tags that update on navigation. - Metadata matches your version: React 19 native
<title>/<meta>/<link>tags for the basics, react-helmet-async for React 18 or advanced needs (never the deprecatedreact-helmet), or the Next.js Metadata API if on Next.js. - If server-rendered, you’re hydrating with
hydrateRoot(notcreateRoot), and any dev-mode hydration mismatch warning is treated as a bug to fix. - Client-side “not found” routes return a real 404 (or a
noindex), not a soft-404 with a200status. - JavaScript and CSS are not blocked in
robots.txt(Google won’t render from blocked files). - No critical content depends on cookies / localStorage / sessionStorage (the renderer is stateless).
- Verified in URL Inspection: the rendered HTML and screenshot show your real content.
- Coverage report checked for “Discovered, currently not indexed” (render backlog) and duplicate clusters (render-timeout trap).
The mental models
1. The only question that matters: what’s in the raw HTML? View Source before any JavaScript runs is what a first-fetch crawler — and most AI crawlers, forever — see. If your content isn’t there, you have a React SEO problem regardless of how good the page looks in your browser.
2. Rendering is a separate, fallible step. Crawl → render → index. CSR puts 100% of your content on the far side of the render step, which is queued, delayed, stateless, and can time out. SSR/SSG move your content before that step. Don’t over-trust the “two waves” model — Splitt himself called it an oversimplification.
3. The boilerplate-duplicate failure mode. Slow content + render timeout = every URL renders as header/nav/footer only = Google sees duplicates. The fix is structural: load content first, or stop depending on the render step.
4. The rendering decision tree.
- Public content that must rank or be cited by AI → SSG (static) or SSR (fresh).
- Mostly static (blog, docs, marketing) → SSG, or ISR on a timer.
- Frequently changing, must be fresh → SSR.
- Logged-in dashboard, not meant to be indexed → CSR is fine.
- New build that needs SEO → reach for Next.js / Remix, not dynamic rendering.
5. HTML-first, JS-second for every SEO signal. Content, links, canonicals, titles, structured data — get them into the server-rendered HTML. Treat JS-injected SEO signals (including JS canonical tags and react-helmet on CSR) as a fallback, not the plan: Google sees them late, and AI crawlers not at all.
React SEO — cheat sheet
Rendering modes at a glance
| Mode | Content in initial HTML? | SEO risk | Use for |
|---|---|---|---|
| CSR (raw React) | No | Highest | Logged-in dashboards, not-indexed apps |
| Pre-rendering (react-snap) | Yes (build time) | Low | Small, mostly-static sites |
| SSG | Yes (build time) | Lowest | Blogs, docs, marketing |
| SSR | Yes (per request) | Low | Fresh, dynamic content |
| ISR / hybrid (Next.js) | Yes | Low | Hourly/daily content |
| Dynamic rendering | Bot-only | Deprecated | Don’t — use SSR/SSG/hydration |
Common React SEO mistakes → fixes
| Mistake | Fix |
|---|---|
| Content only in rendered DOM (CSR) | SSR / SSG / pre-render |
Hash URLs (/#/path) | History API (BrowserRouter) |
onClick navigation, no anchor | Real <a href> / React Router <Link> |
| Meta tags don’t update on route | React 19: native <title>/<meta>/<link>. Older/advanced: react-helmet-async. Next.js: Metadata API |
react-helmet (original, any React version) | Switch to react-helmet-async or React 19 native tags |
createRoot used on server-rendered HTML | Use hydrateRoot instead — createRoot discards the server markup |
| Hydration mismatch warning suppressed | Treat it as a bug and fix the server/client difference |
| Slow content → render timeout → dups | Load main content first; move to SSR/SSG |
Client-side 404 returns 200 | Real 404 status or noindex |
Blocked .js / .css in robots.txt | Allow them — Google won’t render blocked files |
| Content gated on cookies/localStorage | Don’t — the renderer is stateless |
Fast rules
- The renderer is evergreen Chromium, queued, stateless, and times out.
- No ranking bonus for SSR — it’s about reliable indexability (Mueller).
- AI-crawler rendering varies by provider → raw HTML is the safest coverage baseline.
- In Next.js, use the Metadata API — not React Helmet.
- Bing renders JS (via Edge) but less reliably; SSR/SSG is the safe call there too.
Check what the server sends before React runs
Put representative indexable routes in urls.txt. This finds CSR shells and missing
server-rendered head markup in the raw response:
while IFS= read -r url; do
html=$(mktemp)
status=$(curl -sS -o "$html" -w '%{http_code}' "$url")
bytes=$(wc -c < "$html" | tr -d ' ')
title_count=$(grep -Eio '<title>[^<]*</title>' "$html" | wc -l | tr -d ' ')
canonical_count=$(grep -Eio '<link[^>]+rel=["'"']canonical["'"'][^>]*>' "$html" | wc -l | tr -d ' ')
printf '%s\t%s\tbytes=%s\ttitles=%s\tcanonicals=%s\n' "$status" "$url" "$bytes" "$title_count" "$canonical_count"
rm -f "$html"
done < urls.txtSmall byte size is only a review signal, not an error by itself. Compare flagged raw responses with rendered HTML and confirm that primary text and crawlable links are present.
Patrick's relevant free tools
- Raw vs. Rendered HTML Checker — See what's in your page's initial HTML versus after JavaScript runs — headless-Chrome rendering only when the page actually needs it, a rendering-strategy verdict (SSR / prerendered / CSR / hybrid), ~15 calibrated JavaScript-SEO checks (noindex, canonicals, robots.txt blocking, links, soft 404s), a side-by-side raw-vs-rendered diff, and shareable reports.
- SEO Incident Simulator — Practice thirty deterministic technical SEO incident investigations — indexability, crawl controls, redirects, sitemaps, markup, caching, DNS, bot verification, rendering, hreflang, and faceted navigation — with clearly labeled fixture evidence and Find → Fix → Verify handoffs.
- Staging vs. Production SEO Diff — Compare matched release URLs across redirects, canonicals, robots directives, hreflang, selected headers, schema eligibility, and raw or optionally rendered content with honest not-evaluated states.
Tools for auditing React SEO
- View Source vs. Inspect Element — the fastest first check. View Source is the raw HTML (what a crawler gets before JS); Inspect Element is the rendered DOM. Content in Inspect but not View Source is JavaScript-dependent.
- URL Inspection (Google Search Console) — the source of truth. Run a live test, then view the rendered HTML, the screenshot, the page resources (what loaded vs. was blocked), and console messages to see exactly what Google rendered.
- Rich Results Test — a quick rendered-HTML and structured-data check for a single URL without verifying the site.
- Chrome DevTools — disable JavaScript (Command Menu → “Disable JavaScript”) and reload to inspect what an HTML-only fetcher receives. This is a coverage check, not proof of any named AI crawler’s current rendering behavior.
- Search Console Coverage report — watch “Discovered, currently not indexed” (render backlog) and duplicate clusters (the render-timeout boilerplate trap).
- JS-rendering crawlers — Ahrefs Site Audit and Screaming Frog SEO Spider (JS-rendering mode) execute JavaScript so you can diff raw vs. rendered HTML across the site.
Mistakes React teams actually make
Concrete patterns I keep seeing in CSR React apps that get shipped, not hypothetical ones. Each is a prevention move — catch it before it costs you indexing.
Shipping raw CRA/Vite CSR for pages that need to rank
Teams ship Create React App or Vite + React straight to production for marketing pages,
blog posts, or product pages — the exact content that needs to show up in search.
Why it’s wrong: the server sends an almost-empty <div id="root"> shell; your real
content only exists after JavaScript runs, so it’s delayed by Google’s render queue and
can be invisible to any AI crawler that fetches initial HTML without client execution.
What to do instead: move
anything that needs to rank or be cited to SSR or SSG — Next.js or Remix are the easiest
paths — and reserve raw CSR for logged-in, non-indexed surfaces like dashboards.
Routing on hash URLs (HashRouter)
Reaching for React Router’s HashRouter because it’s the path of least resistance —
no server config needed, works on any static host. Why it’s wrong: Google can’t
reliably resolve /#/products-style URLs; the old AJAX-crawling scheme that made hash
fragments crawlable is deprecated. What to do instead: use BrowserRouter (the
History API) and make sure the server responds to every route it produces, including a
direct hit or refresh on a deep link.
Building navigation on onClick instead of real anchors
Wiring up navigation with onClick handlers on a <div> or <button>, often because it
was easier to style or avoid default link behavior. Why it’s wrong: Google only
follows real <a href> links — a <div> with a click handler is invisible to crawling,
no matter how it behaves for a mouse. What to do instead: use React Router’s
<Link> component, which renders an actual <a href> under the hood, or a plain anchor
tag for external navigation.
Loading main content behind a slow API waterfall
Fetching the header and nav fast, then chaining several API calls before the actual page content — the part that makes each URL unique — appears. Why it’s wrong: Google’s Web Rendering Service enforces a timeout; if your centerpiece content loads slowly, rendering finishes before it arrives, and Google is left indexing boilerplate-only pages that then get flagged as duplicates of each other — the exact failure mode Gary Illyes described. What to do instead: restructure requests so main content loads first, or remove the dependency on client-side rendering entirely with SSR/SSG.
Still using the original react-helmet
Reaching for react-helmet for per-route <title> and meta tags because it’s the
library every older tutorial recommends. Why it’s wrong: the original package is
unmaintained — no release since 2020 — and has known issues under React 18’s concurrent
rendering and SSR. What to do instead: on React 19, render <title>/<meta>/<link>
directly in your components and let React hoist them (no library needed for the basics).
On React 18, or for advanced needs like SSR context serialization or titleTemplate, use
react-helmet-async, the maintained fork. On Next.js, use its built-in Metadata API and
don’t bolt Helmet on top of it.
Blocking JavaScript or CSS in robots.txt
Blocking /static/js/ or a bundler’s asset folder in robots.txt, sometimes left over
from an old crawl-budget concern or copied from another site’s config. Why it’s
wrong: Google can’t render what it isn’t allowed to fetch — a blocked bundle means the
Web Rendering Service builds an incomplete (or empty) DOM, even though your source code
is fine. What to do instead: allow crawlers to fetch your JS and CSS, and confirm it
with the URL Inspection tool’s page-resources check to see nothing critical is blocked.
Test yourself: React SEO
Five quick questions on making React apps crawlable and indexable. Pick an answer for each, then check.
Resources worth your time
My related writing
- React SEO: Best Practices to Make It SEO-Friendly (Ahrefs) — the React SEO guide on Ahrefs, which I reviewed; this article is the deeper, source-linked treatment.
- JavaScript SEO: A Definitive Guide (Ahrefs) — my full guide to the underlying rendering mechanics: DOM parity, the most-restrictive-directive rule, canonical and meta-tag handling, and rendering choices. Read this for the general case behind React-specific fixes.
- The Beginner’s Guide to Technical SEO (Ahrefs) — where React/JavaScript SEO fits in the bigger picture.
My speaking
- How Search Works (SlideShare) — my walkthrough of crawling, rendering, indexing, and ranking. (My standing disclaimer applies: “This is my understanding of systems… not going to be 100% complete or accurate.”)
From around the industry
- Understand JavaScript SEO Basics (Google) — primary-source docs on SPAs, the History API, and canonical/status-code handling.
- Dynamic Rendering (deprecated) (Google) — why dynamic rendering is a workaround, not a long-term solution.
- Martin Splitt Explains How JavaScript Sites Are Indexed (Search Engine Journal) — the “barebone HTML … boom” explanation of the crawl → render → index flow.
- Gary Illyes on JS-heavy sites and duplicate content (LinkedIn) — the render-timeout-to-duplicates failure mode, in his own words.
- How to fix technical SEO issues on client-side React apps (Search Engine Land) — a real-world case study on auditing and fixing a CSR React app.
- SSR vs. dynamic rendering — no ranking difference (Search Engine Roundtable) — Mueller’s “no SEO ranking bonuses” statement.
- react-helmet-async (npm) — the maintained head-management library for standalone React.
- The new evergreen Bingbot (Microsoft Edge) (Bing) — Bingbot rendering JS via Chromium, like Googlebot.
Videos
- Google Search Central — JavaScript SEO series (YouTube) — Martin Splitt’s official video series covers SEO for React, Angular, and Vue specifically, walking through the crawl → render → index process and the common fixes. Series announcement · Channel
React SEO
React SEO is the practice of making React apps crawlable and indexable. React renders client-side by default, so the raw HTML is near-empty until JavaScript runs — SSR or SSG puts the content back in the initial response where crawlers (and AI bots) can reliably see it.
Related: JavaScript SEO, Headless CMS SEO
React SEO
React SEO is the discipline of making web apps built with React — Meta’s component-based JavaScript library — discoverable and rankable in search engines.
The core problem is rendering. By default (Create React App, Vite + React), React uses client-side rendering (CSR): the server returns a near-empty HTML shell — typically just a <div id="root"></div> — and the browser runs JavaScript to build the actual page. A crawler that fetches the initial HTML sees little or no content until it executes that JavaScript. Google can render React via its Web Rendering Service (an evergreen Chromium), but rendering is queued separately from crawling, can be delayed, and can time out — and most AI crawlers don’t run JavaScript at all.
React SEO is about closing that gap: choosing a rendering strategy that puts content in the initial HTML (server-side rendering or static site generation, most easily via Next.js or Remix), managing <head> metadata per route (react-helmet-async, not the deprecated react-helmet), using History API routing instead of hash URLs, keeping links as real <a href> anchors, and returning correct HTTP status codes for client-side routes. React isn’t bad for SEO — but client-side rendering by default creates issues that have to be understood and addressed.
Related: JavaScript SEO, Headless CMS SEO
Build-time retrieval analysis plus live signals for this exact article. The automatic chunk report includes a deterministic readiness score and is ready without a model download.
Search Console
sampleGA4 traffic (28d)
sampleCloudflare traffic (7d)
sampledCrUX field data (28d, phone)
sampleGoogle NLP entities
localChangelog
Updated Jul 18, 2026.
Editorial summary and recorded change details.Summary
Added the React 19 native metadata-hoisting story (verified live against react.dev and react-helmet-async's own docs) and a version-scoped createRoot vs. hydrateRoot section covering hydration-mismatch handling, closing the gaps the improvement packet flagged around React-the-library vs. its default CSR toolchain.
Change details
-
Added a React 19 native <title>/<meta>/<link> hoisting explainer to the metadata section, scoped by React version (19 native, 18 or advanced needs react-helmet-async, Next.js Metadata API regardless) — verified react-helmet-async is actively maintained (v3.x, React-19-aware) and the original react-helmet is unmaintained since 2020, both via live source checks.
-
Added a new 'Hydration must match exactly' section distinguishing createRoot (CSR) from hydrateRoot (attaches to server-rendered HTML), grounded in react.dev's official caveat that React does not guarantee patching attribute mismatches — updated the checklist, cheat sheet, quiz, and AI summary to match.
-
Clarified that React the library supports CSR/SSR/static/streaming rendering; the empty-shell problem belongs to the default CRA/Vite toolchain, not to React itself.
Full comparison unavailable — no prior snapshot was archived for this revision.