Encited logo
PricingTalk to sales
React SEO: Is React SEO Friendly, and How to Get a React App Indexed (2026 Guide)

React SEO: Is React SEO Friendly, and How to Get a React App Indexed (2026 Guide)

Oct 16, 2026October 16, 2026·Updated Oct 29, 2026October 29, 2026·by Aki from Encited

Is React SEO friendly? It depends on how it renders. What a client-side React app sends crawlers, React 19 metadata, Next.js, React Router and pre-rendering.

You launched a React app and Search Console lists most of its pages as "Discovered - currently not indexed". Every shared link shows the same title, and ChatGPT can't tell anyone what's on your pricing page. React is SEO friendly when the HTML your server sends already holds the page. React SEO comes down to where that HTML gets built: on a server, at build time, or in the visitor's browser.

React shares its crawl problems with every JavaScript framework: JavaScript SEO covers the general case, and SPA SEO covers the single-page app pitfalls that apply whatever you render with. For a React SPA that has to stay a React SPA, pre-rendering for React serves crawlers the finished page.

TL;DR

  • React is SEO friendly when crawlers get finished HTML: from Next.js, React Router in framework mode, a build-time pre-render, or a pre-render layer in front of the app.
  • A client-side rendered React app (Vite, Create React App, many AI-built apps) sends an empty <div id="root"> and one title for every route. Google renders it later. GPTBot, ClaudeBot, PerplexityBot and link previews never do.
  • React 19 moves <title> and <meta> into the head for you, but in a client-rendered app that still happens in the browser.
  • react-helmet hasn't had a release since 2020. react-helmet-async 3 hands tags to React 19's built-in support.
  • Create React App was deprecated for new apps on February 14, 2025.
  • New build: pick a framework that renders HTML. Existing SPA: put a pre-render layer in front of it.

Is React SEO friendly?

React is a UI library, so what a crawler receives depends on your rendering setup. Google, Bing and the AI crawlers all start from the HTML your server returns for a URL.

  • Rendered on a server per request (Next.js, React Router framework mode, TanStack Start): crawlers get the full page.
  • Rendered at build time (static export, React Router's prerender, a Vite SSG plugin): crawlers get the full page for every route the build knew about.
  • Rendered in the browser (a Vite SPA, Create React App, many apps from AI builders): crawlers get an empty container and a script tag.

You can do SEO in React. Send crawlers finished HTML from a server, a build step or a pre-render layer, and they read a React site like any other. A React app that renders only in the browser still gets into Google, slowly and with gaps, and stays blank to the AI crawlers behind ChatGPT, Claude and Perplexity.

Challenges of SEO in React single-page apps (SPAs)

Fetch any route of a client-rendered Vite React app with curl and you get this file, the same one for /, /pricing and /blog/any-post:

html
CopyDownload
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<script type="module" crossorigin src="/assets/index-XXXX.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

React SPA SEO problems trace back to that file.

  1. Every route has the same head. The title is the Vite template's default until JavaScript changes it, and there's no description or Open Graph tag. Link previews on Slack, LinkedIn and X read this file and stop there. If index.html also carries a canonical, every route points at one URL.
  2. Content and links wait for JavaScript. Googlebot queues the URL for rendering and runs your bundle later. Links in the rendered page join the crawl queue after that, so deep routes take longer to find. JavaScript SEO has the measured render delays.
  3. AI crawlers never run it. GPTBot, ClaudeBot and PerplexityBot download JavaScript files without executing them, according to Vercel's AI crawler study. They read the empty #root. Do AI crawlers render JavaScript covers each bot.
  4. Every URL answers 200. Your host serves index.html for any path so React Router can match it, and the path="*" route shows a not-found screen with a 200 status. Google files those URLs as soft 404s.

Data adds a fifth problem for React client-side rendering SEO. A component that fetches in useEffect renders a spinner first. If the API is slow, errors, or sits behind a path blocked in robots.txt, the page Google stores is the spinner.

React server-side rendering, SSG and pre-rendering: which one to pick

Option What crawlers get Pick it when What it costs you
Next.js (App Router) Server-rendered or prerendered HTML per route You're starting a new build or rebuilding anyway Moving routing and data fetching to Next.js conventions
React Router framework mode Server-rendered HTML, or prerendered files with ssr: false Your app already uses React Router Moving routes to route modules and loaders
Vite with build-time pre-rendering Static HTML for every route known at build Marketing pages and docs that change with deploys A route list for dynamic pages, rebuilds when data changes
Pre-render layer in front of the SPA A rendered snapshot of each page An existing SPA or AI-built app you can't rewrite now A monthly service and snapshot refresh rules
Client rendering only An empty #root Logged-in dashboards and tools that don't need to rank Nothing to search

The four rendering models side by side, with response times and costs, are in SSR vs SSG vs pre-rendering.

Next.js for React server-side rendering SEO

In the Next.js App Router, Server Components render on the server, and Client Components are prerendered to HTML on the first load too, so both reach the HTML. Metadata comes from a metadata export or generateMetadata, which only work in Server Components:

tsx
CopyDownload
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const product = await getProduct(slug);
return {
title: product.name,
description: product.summary,
alternates: { canonical: `/products/${slug}` },
};
}

A relative canonical needs metadataBase set in the root layout. Two more Next.js details decide what crawlers get:

  • Streaming metadata. Since Next.js 15.2, slow generateMetadata output can be appended to <body> after the page starts sending. Only bots on Next.js's HTML-limited list get it in <head>, and GPTBot, ClaudeBot and PerplexityBot weren't on that list in October 2026. Set htmlLimitedBots in next.config.ts to a pattern that matches them, or to /.*/ to turn streaming metadata off.
  • Status codes. A streamed response starts with a 200, and the status can't change after that. A notFound() that runs inside a <Suspense> boundary sends a 200 with a noindex tag, which some crawlers file as a soft 404. Call notFound() before anything suspends, or check the slug in proxy.ts (called middleware.ts before Next.js 16), to send a real 404.

Content a Client Component fetches in useEffect still isn't in the HTML, Next.js or not. Effects never run during server rendering.

To check both on a live route, fetch it with the ClaudeBot simulator and confirm the <title> sits inside <head> and the fetched content is in the body.

React Router framework mode (formerly Remix)

In May 2024 the Remix team announced that what they had planned as Remix v3 would ship as React Router v7. React Router 8, released in June 2026, keeps the same framework mode. Each route module can export a loader that runs on the server or during the build, so its data lands in the HTML. A missing record returns a real 404 when you throw from the loader:

ts
CopyDownload
import { data } from "react-router";
import type { Route } from "./+types/product";
export async function loader({ params }: Route.LoaderArgs) {
const product = await getProduct(params.slug);
if (!product) throw data("Not Found", { status: 404 });
return product;
}

With no server at all, set ssr: false and list the paths to render at build time. React Router writes an HTML file for each one, ready for any static host:

ts
CopyDownload
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
async prerender({ getStaticPaths }) {
const slugs = await getProductSlugs();
return [...getStaticPaths(), ...slugs.map((slug) => `/products/${slug}`)];
},
} satisfies Config;

Since React 19, React Router's docs recommend rendering <title> and <meta> in the component over the route's meta export.

Vite React SEO: pre-render at build time

If your app is on Vite and its routes are known at build, render them to HTML during the build. React Router with ssr: false is one way. vite-react-ssg is a static-site generator for React on Vite, and Vite's SSR guide shows how to pre-render routes with the same code as server rendering.

Each dynamic route needs a path list at build time, data that changes after a deploy needs a rebuild, and anything a component loads in useEffect is missing from the output. The older react-snap package, still the tool in many React SEO tutorials, hasn't had an npm release since December 2018. Gatsby still ships releases, but react.dev no longer lists it among its recommended frameworks.

Prerender React SEO for an existing SPA

When a rewrite isn't on the table, put a pre-render layer in front of the app. Crawlers get a snapshot of each page, rendered in a headless browser after React finishes, and people keep getting the SPA. This is React SEO without SSR, and your code doesn't change.

With Encited in front of your React app, search engines and link previews get the finished HTML, and AI crawlers get HTML or Markdown, depending on what they ask for. Setup is a DNS change or an edge snippet for Vercel, Netlify or Cloudflare Workers. Snapshots refresh on rules you set per path (daily, weekly, monthly or quarterly) or when you re-cache a page by hand, and broken pages are never cached or served. If a route waits on slow API calls, set window.prerenderReady = true once the data is in and the renderer waits for it.

Google's documentation says dynamic rendering "was a workaround and not a long-term solution" and recommends server-side rendering, static rendering or hydration for new builds. It isn't cloaking when crawlers and people get similar content; dynamic rendering SEO has the details.

Create React App SEO

The React team deprecated Create React App for new apps on February 14, 2025. It keeps working in maintenance mode, new installs print a deprecation warning, and react.dev points existing apps to a framework or to a build tool such as Vite, Parcel or Rsbuild.

A Create React App site is a client-rendered SPA with the same empty #root as the Vite file above, so moving it to Vite speeds up builds and changes nothing for crawlers. For SEO, move to a framework that renders HTML, or put a pre-render layer in front of the app you have.

Managing dynamic meta tags and head elements with React Helmet and React 19

React 19: render <title> and <meta> in the component

React 19 moves <title>, <meta> and <link> into the document head from wherever you render them, so each page component owns its tags:

tsx
CopyDownload
function ProductPage({ product }) {
const url = `https://example.com/products/${product.slug}`;
return (
<main>
<title>{`${product.name} | Your Store`}</title>
<meta name="description" content={product.summary} />
<link rel="canonical" href={url} />
<meta property="og:title" content={product.name} />
<meta property="og:image" content={product.image} />
<h1>{product.name}</h1>
</main>
);
}

Three rules from the React docs:

  • Render one <title> at a time. If two components render one, React puts both in the head, and what browsers and search engines do with that is undefined.
  • A <title> takes a single string. <title>Results page {page}</title> passes two children and throws an error. Use a template literal, as above.
  • A <meta> or <title> with itemProp stays where you render it, since it describes part of the page.

In a client-rendered app these tags still appear only after JavaScript runs. They're in the first response when a server or a build step renders the component.

React Helmet SEO: react-helmet vs react-helmet-async

React Helmet is the React SEO library most guides use, usually installed with npm install --save react-helmet. That package's last npm release was 6.1.0 in June 2020. react-helmet-async is the maintained fork. Version 3.0.0 (March 2026) checks the React version at runtime: on React 19 it passes tags to React's built-in support and HelmetProvider does nothing, so existing code keeps working. On React 16 to 18 it needs the provider at the root:

tsx
CopyDownload
import { Helmet, HelmetProvider } from "react-helmet-async";
createRoot(document.getElementById("root")!).render(
<HelmetProvider>
<App />
</HelmetProvider>
);
function PricingPage() {
return (
<>
<Helmet>
<title>Pricing | Your Store</title>
<meta name="description" content="Plans and prices for Your Store." />
<link rel="canonical" href="https://example.com/pricing" />
</Helmet>
<h1>Pricing</h1>
</>
);
}

When two <Helmet> blocks set the same tag, the nested or later one wins, so a layout sets the site defaults and each page overrides them. On React 19 you can drop the library and render the tags directly, or keep it for that default-and-override pattern, which the React team names as a reason to keep a metadata library.

What every route's head needs

  • A unique title and meta description. Check any URL with the meta tag audit.
  • One absolute, self-referencing canonical. Take any canonical out of index.html and set it per route, ideally on the server, so Google never sees two.
  • Open Graph and Twitter Card tags in the server HTML, since previewers don't run JavaScript. Test them in the OG image previewer.
  • noindex only from the server. Google may skip rendering a page whose HTML says noindex, so removing it later with React may not work.

React SEO best practices beyond the head

React Router's <Link> and <NavLink> and Next.js's <Link> render <a href> elements, which is the only kind of link Google discovers. A button that calls navigate("/pricing") in its onClick gives crawlers nothing to follow, so keep navigate() for form submits and redirects after an action. Use createBrowserRouter or BrowserRouter for clean paths; the React, Vue and Angular guide covers why hash routes collapse into one URL.

Soft 404s

Send the status from the server where you can, with the React Router loader or the Next.js notFound() call covered above. In a client-rendered app, your path="*" route can use Google's two client-side fixes. With a pre-render layer, that route sets a meta tag the renderer turns into a real 404, shown in the React, Vue and Angular guide.

Structured data

In React, JSON-LD is a component that renders a script tag:

tsx
CopyDownload
function ProductJsonLd({ product }) {
const data = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
image: product.image,
offers: {
"@type": "Offer",
price: product.price,
priceCurrency: "USD",
availability: "https://schema.org/InStock",
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
}}
/>
);
}

The replace stops a </script> inside your data from closing the tag early. Crawlers that don't render never see JSON-LD that React adds in the browser, so render the component on the server or pre-render the page. Google's warning about Product markup added by JavaScript, and how to test it, are in JavaScript SEO.

Sitemaps and robots.txt

A Vite or Create React App build ships no sitemap, so generate one in a build script from your route list plus your data (product slugs, post slugs). Next.js builds one from an app/sitemap.ts file that returns your URLs, and a server-rendered React Router app can serve one from a resource route, a route module with a loader and no component. Reference the sitemap in robots.txt. JavaScript SEO covers which URLs to list, and which paths robots.txt must not block.

Core Web Vitals

Google counts a page as good at LCP within 2.5 seconds, INP of 200 milliseconds or less and CLS of 0.1 or less, at the 75th percentile of page loads. React apps miss them in predictable places:

  • LCP. In a client-rendered app, the largest element can't paint until the bundle downloads, runs and fetches data. Server or build-time rendering puts it in the first response.
  • INP. Hydrating a large tree and re-rendering big components block the main thread. Split routes with React.lazy, and keep the main content out of lazy chunks.
  • CLS. Content that appears after a fetch pushes the page around. Set image dimensions and size skeletons like the content they hold.

Check field data in the Core Web Vitals report in Search Console. JavaScript SEO covers how to find the slow scripts with PageSpeed Insights and Chrome DevTools.

React apps from AI builders like Lovable and Bolt

Lovable, Bolt and similar AI builders write React code. Lovable's original stack, and many apps built with Bolt, are Vite single-page apps, so the SPA problems above apply.

Lovable now runs two stacks. Projects created before April 2026 use Vite with client-side rendering. Projects on TanStack Start render on the server per route, but the generated code usually loads data from Supabase in the browser, so products, posts and listings arrive after the HTML. Fix Lovable SEO walks through both. To check yours, open View Source on the live site: an empty <div id="root"></div> means client-side rendering.

To check without reading source code, run a published page through the free SEO audit, which shows whether ChatGPT, Claude and Perplexity can read it.

A pre-render layer works on either stack and on any builder's output, because it renders the published site. Setup for Lovable is on pre-rendering for Lovable.

How to check what Google and AI crawlers get from your React app

  1. Check three kinds of route. View Source on the homepage, a dynamic route such as /products/:slug and a URL that doesn't exist. Each needs its own title and canonical, and the dynamic route needs its data in the HTML.

  2. Find data that loads in the browser. In Chrome DevTools, open the Network tab, filter by Fetch/XHR and reload. A request that returns the page's products, posts or prices means that content wasn't in the HTML. This catches Supabase calls in AI-built apps and useEffect fetches in Next.js Client Components.

  3. Fetch as a crawler. Check where the title sits:

    bash
    CopyDownload
    curl -s -A "GPTBot" https://example.com/products/some-product | grep -n "<title>"

    No result means GPTBot gets no title. On Next.js, a <title> found far below </head> is streamed metadata.

For the status code check, URL Inspection, the crawler simulator and your crawl logs, follow the JavaScript SEO audit.

Once the fixes are live, request indexing for key pages in URL Inspection. For a whole set of routes, Index Rush gets them in front of Google faster. Then see whether ChatGPT, Perplexity, Gemini and Claude start mentioning your brand with AI visibility tracking.

FAQ

Is SEO possible in React, and is React still bad for SEO in 2026? SEO is possible in React. The trouble comes from client-side rendering. A React app that sends finished HTML through Next.js, React Router or a pre-render layer gets read like any other site. A client-rendered SPA still gets indexed late by Google and stays blank to AI crawlers.

Is React 19 SEO friendly? React 19 makes metadata easier, but a client-rendered React 19 app still sends crawlers an empty #root. Pair it with server rendering, build-time pre-rendering or a pre-render layer.

Can Google crawl and index a client-side rendered React app? Yes, after a delay. Googlebot queues the page for rendering and runs your JavaScript later, often within seconds and sometimes hours. Pages can still be missed when scripts fail, time out or depend on blocked APIs, and AI crawlers don't render at all.

How does React Helmet update head tags on route changes? Each page renders a <Helmet> with its tags. When the route changes, the old page's <Helmet> unmounts and the new one mounts. Helmet merges every mounted instance, lets the nested or later one win for duplicate tags, and writes the result to the document head.

What is the difference between CSR, SSR and pre-rendering for SEO? CSR builds the page in the visitor's browser, so crawlers get an empty shell. SSR builds it on your server for each request. Pre-rendering builds it ahead of time, at deploy or in a headless browser in front of your site, and serves crawlers the result. SSR vs SSG vs pre-rendering compares them in detail.

Should I move my React SPA to Next.js for SEO? Move when you're rebuilding anyway or starting fresh, especially for a content site or store. If the app works and a rewrite would take months, a pre-render layer gets crawlers the full page now, and you can migrate later on your own schedule.

Can you do React SEO without SSR? Yes. Pre-render your routes at build time if they're known in advance, or put a pre-render layer in front of the app. Both send crawlers finished HTML without a Node server.

Is React overkill for a website? For a site that's mostly text, React adds JavaScript the pages don't need. If you keep React, render it to HTML on a server or at build time, so crawlers and visitors get content before the bundle loads.

How do you detect and fix React crawlability and indexing issues? Run the checks above on single routes. For the whole site, a site audit crawls every route and flags duplicate titles, canonical mismatches, soft 404s and broken internal links, so you fix the template once and then request indexing.

Google
Gemini
ChatGPT
Claude
Grok
Meta
Perplexity

Get discovered anywhere search happens

Readable, citable, outranking pages.

Talk to sales
Avatar
How can we help?
Get instant answers to your questions or leave a message for an engineer will reach out
Ask AI about Encited
See our docs
Contact support
Leave a message
We'll get back to you soon
Avatar
Ask AI about Encited
Product questions, troubleshooting and
Thinking
Preview
Drop an image to attach
Powered by ReplyMaven