I built a beautiful React SPA for a client. Fast, animated, great Lighthouse score. Then I searched Google for our own brand name.
Nothing. No description. No rich snippet. Just a bare title tag and a blank meta description, because Googlebot was reading the raw HTML shell before React ever mounted.
The fix, once I found it, was four lines of code. But getting there meant understanding why client-side rendering breaks React SEO in the first place, and which open source SEO tools for developers can actually verify, not just claim, that your app is crawlable. That's what this post covers: diagnosing the problem, fixing meta tags and structured data with open source packages, and validating the result yourself instead of trusting a paid dashboard's word for it.
Why Googlebot Sees an Empty React Page (And How to Check)
Googlebot does render JavaScript, but it does so in a second, delayed rendering pass, and a lot of other crawlers (Bing, LinkedIn, Twitter/X, Slack unfurlers, most SEO scrapers) don't execute JS at all. They read the raw HTML response. If your <title> and meta description are injected client-side via useEffect or a hook, those crawlers see nothing. This is the core React SEO problem almost every SPA runs into.
You can check this yourself without any tool:
curl -s https://your-site.com | grep -A 2 "<title"
If that returns an empty or generic title while your browser shows something different, you've confirmed the problem. This is the single most important open source SEO tool you have: curl. It shows you exactly what a non-JS crawler sees.
Result: Confirmed. The raw HTML had <title>React App</title> and no meta description at all, despite the rendered page looking correct.
Fixing React SEO Meta Tags for SSR and SPA Apps
The fix depends on your rendering strategy. If you're on Next.js App Router, the framework already gives you server-rendered metadata. The trick is generating it dynamically per route instead of hardcoding a static title.
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
type: 'article',
images: [{ url: post.coverImage, width: 1200, height: 630 }],
},
};
}
If you're on a client-rendered SPA (Vite, CRA, plain React Router) with no SSR at all, you have two real options: add a prerendering step (like vite-plugin-ssr or a static prerender at build time), or use a library that manages document head tags reactively so at least browsers and post-JS crawlers get correct tags. This is where a small open source package like @power-seo/react is genuinely useful for React SEO. It's a thin wrapper around setting title/meta/OG tags per route, nothing magical:
import { SEO } from '@power-seo/react';
function BlogPage({ post }) {
return (
<>
<SEO
title={post.title}
description={post.excerpt}
canonical={`https://example.com/blog/${post.slug}`}
/>
<article>{/* content */}</article>
</>
);
}
Result: curl now shows a populated <title> and <meta name="description"> for SSR routes. For pure client-rendered pages, this only fixes what browsers and JS-executing crawlers see. It does not fix the Googlebot-only-crawlers-HTML problem. Be honest with yourself about which category your app falls into before you declare victory.
Structured Data: The Part Every React SEO Checklist Skips
Meta tags get your page indexed. Structured data (JSON-LD) gets you rich results: star ratings, FAQ dropdowns, article bylines in the SERP. Most tutorials show you a hand-written JSON-LD blob and call it done, but hand-written schema breaks silently. A typo in a property name doesn't throw an error, it just quietly fails Google's Rich Results Test months later.
Here's a validated approach using typed builders instead of raw objects:
import { article, faqPage, schemaGraph, toJsonLdString, validateSchema } from '@power-seo/schema';
const graph = schemaGraph([
article({
headline: 'My Blog Post',
datePublished: '2026-01-15',
author: { name: 'Jane Doe', url: 'https://example.com/authors/jane-doe' },
}),
faqPage([
{ question: 'What is SEO?', answer: 'Search engine optimization.' },
]),
]);
const { valid, issues } = validateSchema(graph);
if (!valid) console.error(issues);
const html = toJsonLdString(graph);
The validateSchema() step is the actual value here. It catches malformed schema at build time or in CI, instead of you finding out three months later that your FAQ rich results silently vanished.
Result: Ran it through Google's Rich Results Test after deploying. FAQ and Article types both validated with no warnings.
Open Source SEO Tools for Developers: Auditing Your Site Instead of Paying for a Dashboard
Paid SEO tools like Screaming Frog or Ahrefs are genuinely good at crawling thousands of pages, but for a single Next.js or React project, you don't need a subscription to catch basic issues: orphan pages, missing alt text, oversized images, thin content. You can run these checks programmatically and even fail your CI pipeline on them:
import { auditPage } from '@power-seo/audit';
import { analyzeContent } from '@power-seo/content-analysis';
const audit = auditPage({
url,
title,
metaDescription,
headings,
wordCount,
focusKeyphrase,
});
const content = analyzeContent({ title, metaDescription, content, focusKeyphrase });
if (audit.score < 70) {
console.error(`Audit score too low: ${audit.score}/100`);
process.exit(1);
}
Result: Wired this into a GitHub Action. It now fails the build if a new blog post ships without a meta description or with an SEO score under 70. Caught two missing descriptions before they ever went live.
What I Learned About React SEO
-
curlbefore anything else. Before reaching for a tool, check what a non-JS crawler actually sees. Most React SEO bugs are diagnosed in 30 seconds this way. - SSR and CSR need different fixes. A client-side meta tag library helps browsers and JS-crawlers, but it won't fix Googlebot's HTML-only crawlers or non-Google platforms. Know which problem you're solving.
- Validate structured data, don't just write it. Hand-written JSON-LD fails silently. A validation step in your build or CI catches it before Google does.
- SEO checks belong in CI, not a quarterly audit. Catching a missing meta description in a PR review is free. Catching it three months later after it's cost you rankings is not.
If you want to try this approach, here's the repo: https://github.com/CyberCraftBD/power-seo
Let's Talk
What's your team's actual process for catching React SEO regressions before they ship: manual checklist, CI gate, or nothing until someone notices traffic dropped? I'm curious whether anyone's automated this beyond a single Lighthouse score in their pipeline.
Top comments (0)