Scientyfic World

React SEO in 2026: How to Make a React App Search-Friendly

React does not hurt SEO by itself. What matters is how your React app gets HTML to the browser and to Googlebot. A plain client-rendered app sends an almost empty...

Share:

Get an AI summary of this article

SEO for React application blog feature image

React does not hurt SEO by itself. What matters is how your React app gets HTML to the browser and to Googlebot. A plain client-rendered app sends an almost empty page and builds everything with JavaScript; a server-rendered or prerendered app sends the content in the first response. Most React SEO advice follows from that one difference, so this guide starts there, then covers metadata, routing, status codes and performance, with two small apps I built to show what search engines actually receive.

The first version of this post was built around Create React App, Uglify and the meta keywords tag. All three are obsolete: the React team deprecated Create React App for new apps in February 2025, modern build tools ship their own minifiers, and Google ignores meta keywords.

Step 1: Choose How Pages Are Rendered

ApproachWhat the first response containsGood forExamples
Client-side rendering (SPA)An empty <div id="root"> plus scriptsLogged-in apps, dashboards, toolsVite + React
Prerendering / static generationFull HTML, generated at build timeMarketing pages, docs, blogs, product pages that change rarelyReact Router prerender, Next.js static pages, Astro
Server-side renderingFull HTML, generated per requestContent that changes often or is personalizedNext.js, React Router framework mode

Google can render JavaScript, but it renders after crawling, and its JavaScript SEO guidance still recommends server-side rendering or prerendering where possible. The React team makes the same point for performance: server rendering sends a complete HTML document and less JavaScript. Other crawlers and link-preview bots may not run JavaScript at all.

A practical rule: if a page should rank in search, render its HTML on the server or at build time. If it sits behind a login, client-side rendering is fine.

What a plain SPA sends

I created a Vite React app (Vite 8, React 19.2) whose only component renders a heading, a paragraph, and React 19’s built-in <title> and <meta> elements. After npm run build, this is the page body Googlebot receives before running any JavaScript:

<title>spa</title>
...
<body>
  <div id="root"></div>
  <script type="module" src="/assets/index-....js"></script>
</body>

No heading, no text, and the template’s placeholder title. Loading the same page in headless Chrome showed the content after JavaScript ran, plus a surprise: the DOM contained two <title> elements, React’s and the static spa one from index.html. React’s documentation warns that multiple titles lead to undefined browser and search engine behavior. If you set titles from components, remove the static one from index.html, or better, prerender.

Step 2: Prerender or Server-Render Your Public Pages

You do not need to rewrite a React app to fix this. React Router’s framework mode can prerender selected routes to static HTML while the rest stays a single-page app. I created a project with create-react-router (React Router 8.4), added a product route, and built it with this config:

import type { Config } from "@react-router/dev/config";
import { products } from "./app/products";

export default {
  ssr: false,
  prerender: ["/", ...products.map((p) => `/products/${p.slug}`)],
} satisfies Config;

The product route loads its data and declares its metadata, including a canonical link, through React Router’s meta export. Unknown slugs throw a 404:

import { data } from "react-router";
import type { Route } from "./+types/product";
import { products } from "../products";

export async function loader({ params }: Route.LoaderArgs) {
  const product = products.find((p) => p.slug === params.slug);
  if (!product) throw data("Not found", { status: 404 });
  return { product };
}

export function meta({ loaderData }: Route.MetaArgs) {
  if (!loaderData) return [{ title: "Not found | My Shop" }, { name: "robots", content: "noindex" }];
  const { product } = loaderData;
  return [
    { title: `${product.name} | My Shop` },
    { name: "description", content: product.description },
    { tagName: "link", rel: "canonical", href: `https://example.com/products/${product.slug}` },
  ];
}

export default function Product({ loaderData }: Route.ComponentProps) {
  const { product } = loaderData;
  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </main>
  );
}

The build log listed each prerendered page, and the generated HTML for a product contained everything a crawler needs without JavaScript:

Prerender (html): /products/blue-mug -> build/client/products/blue-mug/index.html

<title>Blue Ceramic Mug | My Shop</title>
<meta name="description" content="A hand-thrown blue ceramic mug, made to order."/>
<link rel="canonical" href="https://example.com/products/blue-mug"/>
<h1>Blue Ceramic Mug</h1>

The project also passed its TypeScript check. With ssr: true instead, the same loaders and meta functions run per request on a server. Next.js gives the same result with its Metadata API; I tested a full Next.js example in technical SEO for front-end developers.

One catch with static hosting: React Router writes an SPA fallback file for paths that were not prerendered, and the usual host rule serves it for every unknown URL with status 200. That turns typos and deleted pages into soft 404s. Prerender every public page you can, and configure the host to return a real 404 for anything that is not an app route.

Step 3: Give Every Page Its Own Metadata

  • Title and description per route. Use your framework’s mechanism (meta in React Router, metadata or generateMetadata in Next.js). In React 19 you can also render <title>, <meta> and <link> inside components and React moves them into the head; libraries like React Helmet are no longer required for that.
  • Canonical URL. An absolute, self-referencing rel="canonical" on each indexable page (canonicalization guide).
  • Do not use meta keywords. Google does not use them.
  • Social previews. Open Graph and Twitter card tags are for link previews on social and chat apps rather than Google Search, and those crawlers often do not run JavaScript, which is another reason to prerender.

A React 19 <title> must contain a single string. <title>Results page {page}</title> throws an error because it creates two children; use a template string instead.

Step 4: Routing and Links

  • Use your router’s link component (<Link>), which renders a real <a href>. Google does not follow onClick navigation on divs or buttons.
  • Use path-based URLs with the History API. Hash routes such as /#/products are not separate URLs to Google.
  • Give every distinct view that should rank its own URL, and link to it from somewhere crawlable.

Step 5: Status Codes and Missing Pages

A client-rendered “Not found” component returns HTTP 200, which Google treats as a soft 404. With server rendering or prerendering, throw a 404 from the loader (as above) or use your framework’s not-found helper. In a pure SPA, Google’s JavaScript guidance suggests redirecting to a URL that returns a real 404, or adding <meta name="robots" content="noindex"> to the error view. Use server-side 301 or 308 redirects for moved URLs rather than client-side navigation.

Step 6: Performance

The old idea that React is fast “because of the virtual DOM” does not help your Core Web Vitals. React apps are slow when they ship too much JavaScript. Google’s ranking systems use Core Web Vitals, with relevance first; users notice speed either way. The levers that matter:

  • Ship less JavaScript. Server Components, prerendering and code splitting with React.lazy and route-based splitting all reduce what the browser must download and run. The plain Vite app above shipped a 220 KB script (69 KB gzipped) for a heading and a paragraph.
  • Make the LCP element appear in HTML. If the main image is injected by JavaScript, the browser cannot start loading it early. Prerender it and give it fetchpriority="high"; do not lazy-load it (web.dev LCP guide).
  • Keep interactions fast. Long renders after a click hurt INP; split heavy work, use transitions for non-urgent updates, and avoid re-rendering large trees.
  • Prevent layout shift. Set width and height on images, and reserve space for content that loads later.
  • Serve assets well. Production builds already minify; add Brotli or gzip and a CDN. I measured what minification and compression save in this test.

Step 7: Images

Use descriptive alt text, explicit dimensions and modern formats such as WebP or AVIF for photos. SVG is the right choice for icons, logos and simple illustrations, but not for photographs. Lazy-load images below the fold with loading="lazy"; Google’s lazy-loading guidance warns against lazy-loading content that is visible on first load, and Googlebot does not scroll, so content must load when it enters the viewport.

Step 8: Structured Data and Sitemaps

Add JSON-LD for the page type (Product, Article, BreadcrumbList) in the server-rendered HTML, matching visible content, and check it with the Rich Results Test. Generate a sitemap of your public routes at build time; React Router’s prerender list and Next.js’s sitemap.ts both make that straightforward.

Step 9: Verify What Google Sees

  1. curl a public URL. If the main text and links are not in the response, you depend on rendering.
  2. Use Search Console’s URL Inspection tool to see the rendered HTML and the canonical Google chose.
  3. Run Lighthouse or PageSpeed Insights for performance and basic SEO checks.
  4. Check that unknown URLs return 404 and moved ones return 301 or 308 with curl -I.

Conclusion

Optimizing a React app for search mostly means not making Google do extra work: prerender or server-render public pages, give each route real metadata and a real URL, return honest status codes, and ship less JavaScript. React 19 and modern frameworks make each of these a few lines of code. If you are still on Create React App, moving to a framework like React Router or Next.js is the single biggest SEO improvement available. For the React fundamentals behind this, see React core concepts; to ship the result in a container, see how to Dockerize a React app.

FAQs

Is React bad for SEO?

No. Client-side-only React apps can be harder for search engines because content appears only after JavaScript runs. Server-side rendering or prerendering removes that problem, and frameworks such as Next.js and React Router support both.

Can Google index a client-side rendered React app?

Often yes, because Googlebot renders JavaScript with a recent version of Chrome. But rendering can be delayed, mistakes like missing links or soft 404s are common, and other crawlers may not run JavaScript, so Google still recommends server-side rendering or prerendering where possible.

Do I still need React Helmet?

Usually not. React 19 supports rendering title, meta and link elements inside components and moves them into the document head, and frameworks provide their own metadata APIs.

What should I use instead of Create React App?

The React team recommends a framework such as Next.js or React Router for most apps, or a build tool such as Vite, Parcel or Rsbuild for custom setups. Create React App was deprecated for new apps in February 2025.

How do I return a 404 from a React app?

With server rendering or prerendering, throw a 404 response from the route loader or use the framework’s not-found helper. In a pure single-page app, redirect to a URL that returns a real 404 or add a noindex robots meta tag to the error view.

Does the virtual DOM make React sites rank better?

No. Rankings do not depend on how a page updates the DOM. Page performance depends mainly on how much JavaScript you ship and how quickly the main content appears.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts