Back to Blog
React
JavaScript
Technical SEO

React SEO Guide: Fixing the Client-Side Rendering Problem

React apps are invisible to search engines by default. Learn the 5 critical SEO issues in React SPAs and how to fix them, from quick wins to full SSR migration.

RankCLI TeamJanuary 23, 202610 min read

React is the most popular JavaScript framework. It's also one of the worst for SEO out of the box.

Here's the problem: when a search engine visits your React SPA, it sees this:

html
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="root"></div>
  <script src="/bundle.js"></script>
</body>
</html>

Your beautiful UI? Hidden inside JavaScript that may or may not get executed.

This guide covers the 5 critical SEO issues in React apps and how to fix them.

Issue 1: Empty Body Content

The Problem

Search engines have limited JavaScript execution budgets. Google can render JavaScript, but:

  • It's slower (days vs seconds for HTML)
  • It's less reliable (timeouts, errors)
  • Not all search engines do it (Bing is inconsistent)

Detection

RankCLI flags this as CLIENT_SIDE_RENDERING or EMPTY_BODY_CONTENT:

bash
rankcli audit --url https://your-react-app.com
# Error: Body appears empty without JavaScript execution

Solutions

You have several options depending on your app's needs. There's no one-size-fits-all answer, so pick what works for your situation.

Option A: Add Prerendering (Quick Win)

For apps with mostly static public pages, prerendering generates HTML at build time:

bash
npm install react-snap
json
// package.json
{
  "scripts": {
    "postbuild": "react-snap"
  },
  "reactSnap": {
    "include": ["/", "/about", "/pricing"]
  }
}

This keeps your existing React setup and adds SEO for key pages.

Option B: Use a Rendering Service

Services like Prerender.io intercept crawler requests and serve pre-rendered HTML. Zero code changes required, just configure at the CDN/server level.

Option C: Add SSR to Your Existing App

If you need dynamic content rendered server-side, you can add SSR without migrating frameworks:

  • Vite SSR: Vite has built-in SSR support. See Vite SSR Guide
  • React Server Components: Available in React 18+ with frameworks that support them

Option D: Use a Meta-Framework

If you're starting fresh or doing a major rewrite, consider frameworks with SSR built-in:

  • Next.js: Full-featured, large ecosystem
  • Remix: Focus on web standards, great data loading
  • Astro: Great for content-heavy sites, partial hydration

Note: Migrating an existing app to a new framework is a significant undertaking. Weigh the SEO benefits against the engineering cost for your specific situation.

Issue 2: Missing or Duplicate Meta Tags

The Problem

React SPAs often have:

  • One title tag for all pages
  • No meta descriptions
  • Missing Open Graph tags

Detection

bash
rankcli audit --url https://your-app.com/about
# Warning: Title tag matches homepage
# Error: Missing meta description

Solution: React Helmet

bash
npm install react-helmet-async
tsx
// main.tsx
import { HelmetProvider } from 'react-helmet-async';

root.render(
  <HelmetProvider>
    <App />
  </HelmetProvider>
);
tsx
// pages/About.tsx
import { Helmet } from 'react-helmet-async';

export function About() {
  return (
    <>
      <Helmet>
        <title>About Us | MyApp</title>
        <meta name="description" content="Learn about our mission..." />
        <meta property="og:title" content="About Us | MyApp" />
        <meta property="og:description" content="Learn about our mission..." />
        <link rel="canonical" href="https://myapp.com/about" />
      </Helmet>

      <h1>About Us</h1>
      {/* ... */}
    </>
  );
}

Best Practice: Create a Reusable Component

tsx
// components/SEO.tsx
interface SEOProps {
  title: string;
  description: string;
  image?: string;
  url?: string;
}

export function SEO({ title, description, image, url }: SEOProps) {
  const siteUrl = 'https://myapp.com';
  const fullUrl = url ? `${siteUrl}${url}` : siteUrl;
  const imageUrl = image || `${siteUrl}/og-default.png`;

  return (
    <Helmet>
      <title>{`${title} | MyApp`}</title>
      <meta name="description" content={description} />

      <meta property="og:title" content={title} />
      <meta property="og:description" content={description} />
      <meta property="og:image" content={imageUrl} />
      <meta property="og:url" content={fullUrl} />
      <meta property="og:type" content="website" />

      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:title" content={title} />
      <meta name="twitter:description" content={description} />
      <meta name="twitter:image" content={imageUrl} />

      <link rel="canonical" href={fullUrl} />
    </Helmet>
  );
}

Issue 3: JavaScript-Only Navigation

The Problem

React Router links work great for users, but crawlers may not follow them if they're not proper <a> tags with href attributes.

Detection

RankCLI automatically detects JavaScript-only navigation patterns:

bash
rankcli audit --url https://your-app.com
# Warning: JavaScript-only navigation detected
# Examples: button with onClick, <a href="#"> with onClick

Solution

Always use <Link> from React Router, not onClick navigation:

tsx
// Bad - not crawlable
<button onClick={() => navigate('/about')}>About</button>

// Good - crawlable
<Link to="/about">About</Link>

If you must use buttons, add a hidden link:

tsx
<button onClick={() => navigate('/about')}>
  About
  <Link to="/about" className="sr-only" tabIndex={-1}>About</Link>
</button>

Issue 4: Missing Structured Data

The Problem

React apps rarely include JSON-LD structured data, missing out on rich results.

Solution

Add JSON-LD in a script tag:

tsx
// components/StructuredData.tsx
interface StructuredDataProps {
  data: Record<string, unknown>;
}

export function StructuredData({ data }: StructuredDataProps) {
  return (
    <Helmet>
      <script type="application/ld+json">
        {JSON.stringify(data)}
      </script>
    </Helmet>
  );
}
tsx
// pages/Product.tsx
<StructuredData data={{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": product.name,
  "description": product.description,
  "offers": {
    "@type": "Offer",
    "price": product.price,
    "priceCurrency": "USD"
  }
}} />

Issue 5: Poor Performance

The Problem

Large JavaScript bundles hurt both user experience and SEO. Core Web Vitals are a ranking factor.

Detection

bash
rankcli audit --url https://your-app.com
# Warning: JavaScript bundle exceeds 500KB
# Warning: Largest Contentful Paint is slow

Solutions

Code Splitting

tsx
// Use React.lazy for route-based splitting
const About = React.lazy(() => import('./pages/About'));
const Pricing = React.lazy(() => import('./pages/Pricing'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/about" element={<About />} />
        <Route path="/pricing" element={<Pricing />} />
      </Routes>
    </Suspense>
  );
}

Optimize Images

tsx
// Use responsive images
<img
  src="/hero-800.jpg"
  srcSet="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
  sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
  alt="Hero image"
  loading="lazy"
/>

Preload Critical Resources

html
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />
<link rel="preload" href="/hero.jpg" as="image" />

The Rendering Decision

How do you decide what approach to take? Here's a framework:

SituationRecommendation
Static marketing pagesPrerendering (react-snap, Vite SSG)
Dynamic content that needs indexingSSR via Vite SSR or rendering service
New project with heavy SEO needsConsider SSR-first framework (Next, Remix, Astro)
Internal/dashboard appReact SPA is fine, no SEO needed
Existing app, limited engineering timeRendering service (Prerender.io)

The key question isn't "React vs Next.js", it's "what pages need to be crawlable, and what's the simplest way to achieve that?"

Quick Wins Checklist

Before any big changes, implement these:

  • Add React Helmet with unique meta tags per page
  • Ensure all navigation uses <Link> components
  • Add robots.txt and sitemap.xml to your public folder
  • Add JSON-LD for your primary content type
  • Enable gzip/brotli compression
  • Add loading="lazy" to below-fold images

Next Steps

  1. Run an audit: npx rankcli audit --url https://your-site.com
  2. Fix quick wins: Meta tags, robots.txt, sitemap
  3. Choose your rendering strategy: Based on your needs, pick prerendering, SSR, or a rendering service
  4. Add to CI/CD: Prevent SEO regressions

Need help with React SEO? RankCLI detects all these issues automatically. Try the free audit.

Try RankCLI

Catch SEO issues before they hurt your rankings. Run your first audit in seconds.