# React SEO: How to Make a React App Rank in Google and AI Search (2026)
Author: Sai Krishna
Author URL: https://superblog.ai/blog/author/sai-krishna/
Published: 2026-11-05
Category: SEO
Category URL: https://superblog.ai/blog/category/seo/
Meta Title: React SEO: How to Make a React App Rank | Superblog
Meta Description: React SEO in 2026: why client-side rendering hurts rankings, the SSR, prerendering and metadata fixes that work, and where your blog should live.
Tags: SEO, subdirectory, developer, blog seo, performance
Tag URLs: SEO (https://superblog.ai/blog/tag/seo/), subdirectory (https://superblog.ai/blog/tag/subdirectory/), developer (https://superblog.ai/blog/tag/developer/), blog seo (https://superblog.ai/blog/tag/blog-seo/), performance (https://superblog.ai/blog/tag/performance/)
URL: https://superblog.ai/blog/react-seo/

React SEO starts with the HTML a crawler receives before any JavaScript runs. In a default React single-page app, that HTML is an empty `<div id="root">` and a script tag. Fix that with server rendering or build-time prerendering, give every route its own title, description and canonical URL, and React ranks like any other site. Then comes the harder part, which this guide also covers: a technically clean app still needs pages that people search for.

This guide is for founders, marketers and developers whose site or product is a React app: Create React App, Vite, React Router, or something generated by an AI builder. Part one is the technical fix list, with code. Part two is the content side, and where a React team should run its blog.

## Is React bad for SEO?

No. React is a UI library, and Google indexes plenty of React-built sites. The problem is the default rendering mode most React apps ship with.

Here is the `index.html` that the official Vite React template generates, exactly as the server sends it:

```html html
<!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 + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

Every URL in the app returns this same file. No headline, no copy, no links, and the same title on the pricing page as on the homepage. Your content exists only after the browser downloads the bundle, runs it, and fetches whatever data the components ask for.

A person with a browser never notices. A crawler does, and that is where the ranking problems start.

## Why client-side rendering hurts React SEO

Four separate mechanisms cause the damage. They compound, so a client-rendered app usually suffers from all of them at once.

### Google renders your pages later, in a queue

Google's [JavaScript SEO documentation](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) describes three phases: crawling, rendering and indexing. "Googlebot queues pages for both crawling and rendering." On rendering time, it says: "The page may stay on this queue for a few seconds, but it can take longer than that." A headless Chromium executes your JavaScript only when Google has resources available.

So Google can index a React SPA, but every page passes through an extra step that server-rendered pages skip. If the render fails, times out, or hits an API that blocks Googlebot, the indexed version of the page is the empty shell.

### Titles and descriptions arrive late, or never

In a client-rendered app, per-route titles and meta descriptions are set by JavaScript after load. Google says this is allowed: "You can use JavaScript to set or change the meta description as well as the `<title>` element." The tags still only exist after rendering, though, and anything that reads the raw HTML sees `Vite + React + TS` on every page.

Canonical URLs are stricter. Google's guidance reads: "The best way to set the canonical URL is to use HTML." It also says "you shouldn't use JavaScript to change the canonical URL to something else."

### Some crawlers never run JavaScript at all

Google's own documentation states that "not all bots can run JavaScript." Social preview bots are the familiar example. The Next.js docs list `facebookexternalhit` as an HTML-limited bot that cannot execute JavaScript, which is why a shared link to a client-rendered page often shows a blank card.

AI crawlers matter more now. Vercel's [December 2024 analysis of AI crawler traffic](https://vercel.com/blog/the-rise-of-the-ai-crawler) concluded that "none of the major AI crawlers currently render JavaScript," naming OpenAI's and Anthropic's crawlers among them. The same analysis found that Google's Gemini uses Googlebot's rendering infrastructure.

That study is nearly two years old and crawler behavior changes, so recheck before you rely on it. Serving complete HTML makes the question irrelevant.

For reference, the crawlers you are dealing with:

- **OpenAI:** GPTBot gathers training data, and OAI-SearchBot is the crawler that decides whether ChatGPT search can cite your pages.
- **Anthropic:** ClaudeBot crawls for training, Claude-User fetches a page when a user asks Claude to, and Claude-SearchBot powers Claude's web search.
- **Google:** the Google-Extended token controls whether Gemini models train on your pages and use them for grounding in Gemini Apps and Vertex AI. It has no effect on Google Search, and AI Overviews are part of Search, so regular Googlebot crawling and ranking decide whether they use you.

Our [guide to AI crawlers](https://superblog.ai/blog/ai-crawlers-guide/) covers each one and how to control them in robots.txt.

### Links and status codes break in SPAs

Two more failures are common in hand-rolled React routing:

1. **Links that are not links.** Google states that it "can only discover your links if they are `<a>` HTML elements with an `href` attribute." A `<div onClick={() => navigate('/pricing')}>` is invisible to the crawler. Use React Router's `<Link>` or Next.js's `<Link>`, which render real anchors.
2. **Every URL returns 200.** A static host serving one `index.html` for all paths sends a 200 status for `/this-page-does-not-exist`. Google calls the result a soft 404. Its documented fixes for client-rendered apps are a JavaScript redirect to a URL that returns a real 404, or adding `<meta name="robots" content="noindex">` to error views with JavaScript.

Also avoid hash routing ( `/#/pricing`). Google's guidance is to "use the History API to implement routing" and "don't use fragments to load different page content."

## React SEO checklist: the fixes, in order of impact

Work down this list. The first item solves most of what the sections above describe, and everything after it assumes you have made a decision on it.

### 1\. Serve HTML from the server or the build

Google's position is direct: "server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers." React's own team moved the same way. In February 2025 the React blog announced: "Today, we're deprecating Create React App for new apps," and recommended frameworks such as Next.js, React Router and Expo, or build tools like Vite, Parcel and Rsbuild.

Your realistic options, by starting point:

Your app todayMove that fixes renderingEffortVite SPA using React Router `<Routes>` (move a CRA app to Vite first)React Router framework mode with `prerender`Medium: one-time setup, then route-by-route migrationMarketing pages and app in one SPAMove marketing pages to Next.js or Astro, keep the app as isMedium to highNext.js pages that fetch their content in the browser ( `useEffect` or client-side data hooks)Fetch the data in Server Components so it ships in the HTMLLow to mediumContent-heavy site with a few interactive widgetsAstro, with React components as islandsMedium

**React Router framework mode.** This is the shortest path for most Vite SPAs, because you keep React Router. Framework mode wraps the router in a Vite plugin and supports, in the docs' words, "SPA, SSR, and static rendering strategies." React Router publishes an [adoption guide for apps using `<Routes>`](https://reactrouter.com/upgrading/component-routes) that migrates routes one at a time.

Once you are on framework mode, prerendering is a config change in `react-router.config.ts`. This keeps your app a static SPA but writes real HTML for the routes you list:

```ts ts
import type { Config } from "@react-router/dev/config";

export default {
  ssr: false,
  prerender: ["/", "/pricing", "/features", "/about"],
} satisfies Config;

```

The [pre-rendering docs](https://reactrouter.com/how-to/pre-rendering) note that `prerender: true` covers static paths only and "will not include any dynamic paths (i.e., `/blog/:slug`) because the parameter values are unknown." For those, pass an array or a function that returns the full list. Prerendering is available in framework mode only, not in data or declarative mode.

**Next.js.** Server Components render on the server by default, and metadata is resolved there too. Next.js states that `metadata` and `generateMetadata` are "only supported in Server Components" so that metadata lands "in the initial HTML response." Our [Next.js SEO guide](https://superblog.ai/blog/nextjs-seo/) covers the framework in depth. Per-route metadata looks like this:

```tsx tsx
// app/products/[id]/page.tsx
import type { Metadata } from 'next'

type Props = { params: Promise<{ id: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { id } = await params
  const product = await getProduct(id)

  return {
    title: product.name,
    description: product.summary,
    alternates: { canonical: `/products/${id}` },
  }
}

```

The relative canonical resolves against `metadataBase`, which you set once in the root layout. Without it, Next.js throws a build error on relative URL fields.

**Astro with React islands.** Astro renders components to static HTML and strips client-side JavaScript unless you opt in. You add React with `npx astro add react` and hydrate only the parts that need interactivity, such as `<PricingCalculator client:visible />`. This fits marketing sites well. It does not fit a dashboard-style product, which belongs in a framework built for apps.

### 2\. Prerender as a stopgap if you cannot migrate yet

If a framework migration is months away, a prerendering step that snapshots your routes to HTML at build time buys you crawlable pages now. Avoid the older pattern of serving a separate rendered version only to bots. Google's [dynamic rendering page](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering) says it "was a workaround and not a long-term solution," and recommends "server-side rendering, static rendering, or hydration" instead.

### 3\. Give every route its own title, description and canonical

Since React 19 (stable December 2024), you can render `<title>`, `<meta>` and `<link>` inside any component, and React hoists them into `<head>`:

```jsx jsx
export default function PricingPage() {
  return (
    <main>
      <title>Pricing | Acme</title>
      <meta name="description" content="Compare Acme plans for teams of every size." />
      <link rel="canonical" href="https://acme.com/pricing" />
      <h1>Pricing</h1>
      {/* ... */}
    </main>
  );
}

```

React Router's docs now state: "Since React 19, using the built-in `<meta>` element is recommended over the use of the route module's `meta` export." React's own release notes add that libraries like `react-helmet` still help when you need route-level overrides of generic tags.

One caveat matters for SEO. These tags change _how_ you manage metadata, not _when_ it reaches the crawler. In a client-only app they still appear after JavaScript runs. Pair them with item 1 so they ship in the initial HTML.

React's [`<title>` reference](https://react.dev/reference/react-dom/components/title) also warns: "Only render a single `<title>` at a time." Two components rendering titles at once leave browsers and search engines with undefined behavior.

### 4\. Publish a sitemap and point robots.txt at it

React has no sitemap generator. In Next.js, an `app/sitemap.ts` file that returns a `MetadataRoute.Sitemap` array produces `/sitemap.xml`. In a Vite app, generate `sitemap.xml` into the build output from your route list, and include dynamic routes from your data.

Google's [sitemap rules](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap): a single sitemap is limited to "50MB (uncompressed) or 50,000 URLs," and you should "use fully-qualified, absolute URLs." Add a `Sitemap: https://yoursite.com/sitemap.xml` line to robots.txt so crawlers find it.

### 5\. Use real links and real error responses

Use `<Link>` components that render `<a href>`, never click handlers on generic elements. Configure your host or server to return a real 404 for unknown paths when you can. When you cannot, apply one of the two soft-404 fixes from Google listed above.

### 6\. Keep Core Web Vitals in the green

Client-rendered apps tend to fail Largest Contentful Paint, because the main content waits on the JavaScript bundle and then on an API call. Google's [Core Web Vitals thresholds](https://web.dev/articles/vitals), measured at the 75th percentile of page loads:

- **LCP:** 2.5 seconds or less
- **INP:** 200 milliseconds or less
- **CLS:** 0.1 or less

Server rendering helps LCP directly. The React team made a related point when it retired Create React App: server rendering sends "a full HTML document which produces a faster First Contentful Paint (FCP)." Beyond that, split code by route, set explicit image dimensions to prevent layout shift, and keep heavy third-party scripts off landing pages.

### 7\. Check what crawlers actually receive

Do not trust the browser's Elements panel, which shows the DOM after JavaScript. Check the raw response instead:

```bash bash
curl -s https://yoursite.com/pricing | grep -iE "<title>|<h1|canonical"

```

If the title, H1 and canonical are not in that output, crawlers that skip JavaScript do not see them either. For Google specifically, open Search Console, inspect the URL, and check the HTML Google produced after rendering. Our [explainer on SEO crawling](https://superblog.ai/blog/seo-crawling/) goes deeper on crawl budget and rendering.

## Rendering fixes do not create rankings

Once your app serves clean HTML, Google can read your homepage, pricing page and feature pages. That is all it can read, because that is all you have.

Those pages rank for your brand name. They do not rank for what buyers type before they know your brand: "how to reconcile Stripe payouts," "X software for agencies," "Competitor vs Competitor," "X alternative." Those queries are answered by articles, and a site with six pages has no article to rank.

This is the step technical React SEO guides skip. The fix list above is necessary, and it is the floor. Organic growth comes from publishing useful pages on a steady schedule, on your own domain, for months. That requires a blog, and a React app is an awkward place to build one.

Building a blog inside the app means your team now owns:

1. A content model, a database or MDX pipeline, and an editor non-developers can use.
2. Post templates, category and tag archives, pagination and an RSS feed.
3. Per-post metadata, JSON-LD structured data, a sitemap that updates on every publish, and canonical logic.
4. Image optimization, Open Graph images, and the Core Web Vitals of every template.
5. A deploy of the product every time marketing publishes a post.

None of that is your product. All of it competes with your product for engineering time.

A managed platform takes that list off your engineers. Superblog, option 2 below, runs the blog at `/blog` on your domain after a one-time routing change.

If your app came out of an AI builder, the rendering details differ by tool. Lovable, for example, changed how it serves pages in 2026; our [Lovable SEO guide](https://superblog.ai/blog/lovable-seo/) covers that platform specifically. The content gap is the same regardless of who generated the code.

## Where the blog for a React app should live

There are four realistic setups. They differ most on who publishes, who maintains it, and whether the authority it earns stays on your main domain.

### 1\. Build the blog into your React app with SSR or SSG

If you have already moved to Next.js, React Router framework mode or Astro, adding MDX posts or a CMS-backed blog route is the native option. Rendering is solved, and the blog sits on your domain.

The cost is everything in the five-item list above. It fits teams where developers write the posts, or where engineering has dedicated time to own a publishing system. It fits poorly when a marketer needs to publish three times a week without a pull request. If you are on Next.js and still want to compare approaches, see our [Next.js walkthrough](https://superblog.ai/blog/add-blog-to-nextjs/).

### 2\. Run a managed blog at yoursite.com/blog with Superblog

Your React components stay exactly as they are. A routing rule on your host sends `/blog` and everything under it to [Superblog](https://superblog.ai), which serves a complete blog: editor, templates, hosting and SEO output. Readers and crawlers see one domain.

This is the option for teams that want the blog's SEO to accrue to the main domain without building or maintaining a publishing system. Marketers publish from a WYSIWYG editor, and the React app is never redeployed for a post. The trade-off: Superblog is built for blogs and content, not for building other parts of your site, and the blog's templates are Superblog's rather than your React components. Details are in the next section.

### 3\. Pair a headless CMS with your own frontend

Contentful, Sanity, Strapi and similar tools give writers a real editor. Your developers still build and maintain every template, the metadata and structured data output, the sitemap and the rendering. Choose this when the blog must share your design system component for component and you have frontend capacity for it. Our [headless blog breakdown](https://superblog.ai/blog/headless-blog/) covers the trade-offs.

### 4\. Host the blog on a subdomain

A hosted tool at `blog.yoursite.com` is quick to set up and needs no routing change. The catch is that the blog lives on a separate hostname, so the links and rankings it earns attach to `blog.yoursite.com` instead of building up `yoursite.com` the way a `/blog` path does. The full argument is in [subdomain vs subdirectory](https://superblog.ai/blog/subdomain-vs-subdirectory/).

SetupWho publishesWho maintains the blogDomain authorityBlog built into your SSR React appDevelopers, or marketers via a CMS you integrateYour engineering teamStays on main domainSuperblog at `/blog`Marketers, from an editorSuperblogStays on main domainHeadless CMS + your frontendMarketers write, developers ship templatesYour engineering teamStays on main domainHosted blog on a subdomainMarketersThe vendorSplit across two hostnames

## How Superblog serves a blog next to your React app

The mechanism is a reverse proxy rule. Your host forwards requests for `/blog` and `/blog/*` to your Superblog origin and returns the response under your own URL. Your React app never sees those requests. The full mechanics, including Nginx and Cloudflare Workers, are in our [subdirectory blog hosting guide](https://superblog.ai/blog/subdirectory-blog-hosting/).

On Vercel, the rules go in `vercel.json`. If your SPA already has a catch-all rewrite to `index.html`, keep the blog rules above it:

```json json
{
  "rewrites": [
    { "source": "/blog", "destination": "https://yourblog.superblog.click/" },
    { "source": "/blog/:path*", "destination": "https://yourblog.superblog.click/:path*" },
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

```

On Netlify, the same applies in `netlify.toml`. Netlify's docs say the SPA rewrite "is typically the last rule listed," so the blog rules come first:

```toml toml
[[redirects]]
  from = "/blog"
  to = "https://yourblog.superblog.click/"
  status = 200
  force = true

[[redirects]]
  from = "/blog/*"
  to = "https://yourblog.superblog.click/:splat"
  status = 200
  force = true

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

```

The Superblog dashboard generates these rules with your blog's origin filled in, for Vercel, Netlify, Next.js, Nginx, Apache, Cloudflare, AWS Amplify, CloudFront, Firebase and others. For a Vercel-hosted app, our [Vercel walkthrough](https://superblog.ai/blog/add-blog-to-vercel/) walks through it end to end, and the [React integration overview](https://superblog.ai/use-cases/blog-for-react) summarizes the setup.

What arrives at `/blog` is the opposite of the empty shell from the start of this guide. Superblog builds every page ahead of time as static HTML and delivers it from more than 200 CDN edge locations, so crawlers that never execute JavaScript get the full article in the first response. On top of that, without configuration:

- **Structured data on every post.** JSON-LD for Article (or BlogPosting), WebPage, Organization, ImageObject and FAQ is generated automatically.
- **Sitemaps and IndexNow.** XML sitemaps rebuild on every deploy, and publishing a post notifies Bing, Yandex and other IndexNow engines immediately.
- **A machine-readable layer for AI engines.** An `/llms.txt` index at `yoursite.com/blog/llms.txt`, plus a plain-markdown copy of each post at its own URL with `.md` appended. Together with the server-rendered HTML, this gives ChatGPT, Claude and Google AI Overviews clean text to work from.
- **Per-post overrides.** Each post has its own fields for meta title, description, canonical URL and Open Graph tags, and a SERP preview shows the search result before you publish.
- **90+ Lighthouse scores**, with automatic WebP image conversion.
- **Multilingual SEO on Super and above**, with hreflang tags and per-language sitemaps.

Superblog's Pro plan is $49/month and includes 5 team members, scheduled publishing and privacy-friendly analytics. The $99/month Super plan brings multilingual SEO, Zapier and REST API access. Subdirectory hosting is on every paid plan, and you can start with a 7-day free trial without a credit card.

## FAQ

### Can a React app rank on Google?

Yes. React's default client-side rendering is what holds it back, because the server sends an empty HTML shell. Server rendering or prerendering with Next.js, React Router framework mode or Astro fixes it.

### Does Google index client-side rendered React apps?

Yes, after a rendering step. Google queues pages for rendering and runs your JavaScript in headless Chromium when resources allow. Pages that fail to render, or depend on blocked APIs, get indexed as the empty shell.

### Do I need Next.js for React SEO?

No. Next.js is one option. React Router framework mode with `prerender` keeps a Vite app on its existing router, and Astro suits content-heavy marketing sites. What you need is HTML in the initial response.

### Do I still need React Helmet?

For most new code, no. React 19 renders `<title>`, `<meta>` and `<link>` from any component and hoists them to `<head>`. A metadata library still helps when you need route-level overrides of default tags.

### Can ChatGPT and Claude read my React app?

Only reliably if your pages return content in the HTML. Vercel's December 2024 crawler analysis found that the major AI crawlers it measured, including OpenAI's and Anthropic's, did not render JavaScript. Server-rendered or static pages avoid the problem entirely.

### What is the quickest way to get a blog on a React site?

Route `/blog` to a managed blog platform with a reverse proxy rule on your host. With Superblog on Vercel or Netlify, that is two rewrite rules in your host config, and none of your React components change.

## Render first, then build the blog

Fix rendering first, because nothing else on this list works without it. Then add per-route metadata, a sitemap, real links and green Core Web Vitals. After that, the gains come from content.

If you want that content on your domain without building a CMS into your React app, [set up your blog on Superblog](https://superblog.ai) and serve it at `/blog`. Your app keeps shipping product. Your blog starts earning the searches your app pages never will.


---
This blog is powered by Superblog. Visit https://superblog.ai to know more.
---

