Skip to main content

Command Palette

Search for a command to run...

SEO for React Apps: Why Google Can't See Yours (And How to Fix It)

Updated
6 min read
SEO for React Apps: Why Google Can't See Yours (And How to Fix It)
M
I build websites and apps using Javascript. I love making things work fast and look great. Let's create something cool

I spent 11 hours debugging why my React portfolio wasn't showing up in Google Search Console. Every page showed as "Crawled: currently not indexed." The fix? Four lines of metadata I had completely neglected. If you've ever shipped a React app and wondered why it's invisible to search engines: this is the SEO for React apps guide you need.

By the end, you'll understand why React apps fail at SEO by default, how to implement the core fixes yourself, and one lightweight tool worth knowing about if you want to avoid reinventing the wheel.

Why SEO for React Apps Is Broken Out of the Box

Here's the uncomfortable truth: Googlebot is lazy. It prefers HTML it can read immediately: not a <div id="root"></div> that only reveals its contents after JavaScript executes.

Traditional React apps (built with Create React App or plain Vite) are client-side rendered (CSR). The server sends a nearly empty HTML shell, and your JavaScript fills it in later. By the time the crawler has moved on, your <title>, <meta description>, and Open Graph tags may still not exist.

The result: every page in your app looks identical to Googlebot: because they all start from the same empty index.html.

Improving SEO for React apps comes down to three core fixes:

  1. Dynamic meta tags per route: each page needs its own unique title and description

  2. Structured data (JSON-LD): so Google understands your content type

  3. Sitemap + canonical URLs: so crawlers know which pages exist and which URL is authoritative

Let's tackle each one with working code.

Fix #1: Dynamic Meta Tags: The Foundation of React SEO

The most common SEO mistake in React apps is setting <title>My App</title> once in index.html and calling it done. Every route shares that title. Google sees them as duplicates and may not index any of them.

Install react-helmet-async: the actively maintained fork of react-helmet:

npm install react-helmet-async

Wrap your app root in the provider:

// main.jsx
import { HelmetProvider } from 'react-helmet-async';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
  <HelmetProvider>
    <App />
  </HelmetProvider>
);

Then give each page component its own <head>:

// pages/BlogPost.jsx
import { Helmet } from 'react-helmet-async';

export default function BlogPost({ post }) {
  return (
    <>
      <Helmet>
        <title>{post.title} | My Dev Blog</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta property="og:type" content="article" />
        <link rel="canonical" href={`https://yourdomain.com/blog/${post.slug}`} />
      </Helmet>
      <article>{/* post content */}</article>
    </>
  );
}

Result: Each route now has unique, crawlable metadata. No more "duplicate title tags" warnings in Search Console. This single fix resolves the most common React SEO indexing failures.

Fix #2: JSON-LD Structured Data for React

Structured data is how you tell Google what kind of content it's looking at: a blog post, a product, a person. When Google understands your content type, it can show rich results: star ratings, breadcrumbs, article dates. These directly improve click-through rate in search results.

This reusable component handles JSON-LD injection for your entire React app:

// components/JsonLd.jsx
export default function JsonLd({ data }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

Use it alongside Helmet in any page component:

// pages/BlogPost.jsx
import JsonLd from '../components/JsonLd';

const schema = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": post.title,
  "author": {
    "@type": "Person",
    "name": "Your Name"
  },
  "datePublished": post.publishedAt,
  "description": post.excerpt,
  "url": `https://yourdomain.com/blog/${post.slug}`
};

return (
  <>
    <Helmet>...</Helmet>
    <JsonLd data={schema} />
    <article>...</article>
  </>
);

Result: Google stops treating your content as an unknown page. Rich snippets become eligible. This is one of the most underused SEO improvements for React apps: it takes 20 minutes and the payoff is measurable.

Fix #3: Sitemaps and Canonical URLs in React

Even with perfect meta tags, Google can't index pages it doesn't know exist. A sitemap is your React app's table of contents for crawlers: without one, you're relying entirely on Googlebot discovering links organically.

For Vite-based React projects, install the sitemap plugin:

npm install vite-plugin-sitemap

Configure it in vite.config.js:

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import sitemap from 'vite-plugin-sitemap';

export default defineConfig({
  plugins: [
    react(),
    sitemap({
      hostname: 'https://yourdomain.com',
      dynamicRoutes: [
        '/blog/how-to-fix-react-seo',
        '/blog/vite-performance-tips',
        '/projects/my-saas-app',
      ],
    }),
  ],
});

This auto-generates a sitemap.xml at build time. Submit it to Google Search Console under Sitemaps.

For canonical URLs: include <link rel="canonical"> on every page, especially if your content is reachable at multiple paths (trailing slashes, pagination, query strings). Without canonicals, Google may index the wrong version: or penalize you for duplication.

When You Want React SEO Pre-Wired

After wiring this up across two projects, I got tired of copying the same boilerplate. I came across power-seo: a small npm package that bundles Helmet management, JSON-LD injection, and canonical handling into one component. It's the same patterns described above, just pre-assembled.

npm install power-seo
import { SEO } from 'power-seo';

<SEO
  title="My Blog Post Title"
  description="A practical guide to fixing React SEO"
  canonical="https://yourdomain.com/blog/my-post"
  type="article"
  schema={{ /* your JSON-LD object */ }}
/>

The full write-up with additional examples lives at ccbd.dev: worth reading for the deeper explanation of why each piece is needed, not just how to use the package.

Use it if you want faster setup. Build it yourself if you want full control. Either way, the underlying SEO mechanics for React are what I've walked through above.

Key Takeaways: SEO for React Apps

  • CSR React apps are invisible to slow crawlers by default: every indexing problem I've seen traced back to missing meta tags, missing structured data, or both

  • Dynamic meta tags per route are non-negotiable: a shared title across all routes is one of the most common and damaging React SEO mistakes

  • Canonical URLs prevent silent duplicate-content penalties: without them, Google quietly deprioritizes your pages

  • JSON-LD is lower-effort than it looks: one reusable component covers your whole app; the rich result potential is worth the 20 minutes

  • Sitemap submission is the last mile: perfect metadata still leaves Google discovering your pages slowly without it

If you want to try the pre-packaged approach, here's the repo: https://github.com/CyberCraftBD/power-seo

Let's Talk

What's the worst SEO issue you've hit with a React or SPA project? I'm particularly curious whether anyone has dealt with dynamic routes and differing crawler behavior between Vercel and Netlify: the edge cases there took me a while to untangle. Drop your experience in the comments.

More from this blog

P

Power SEO Free Tool

30 posts