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.
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:
bashrankcli 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:
bashnpm 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
bashrankcli audit --url https://your-app.com/about # Warning: Title tag matches homepage # Error: Missing meta description
Solution: React Helmet
bashnpm 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:
bashrankcli 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
bashrankcli 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:
| Situation | Recommendation |
|---|---|
| Static marketing pages | Prerendering (react-snap, Vite SSG) |
| Dynamic content that needs indexing | SSR via Vite SSR or rendering service |
| New project with heavy SEO needs | Consider SSR-first framework (Next, Remix, Astro) |
| Internal/dashboard app | React SPA is fine, no SEO needed |
| Existing app, limited engineering time | Rendering 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
- Run an audit:
npx rankcli audit --url https://your-site.com - Fix quick wins: Meta tags, robots.txt, sitemap
- Choose your rendering strategy: Based on your needs, pick prerendering, SSR, or a rendering service
- 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.