I spent way too long debugging why Google couldn't see my React app. The fix was 4 lines of code.
Turns out, the problem wasn't Google. It was me assuming client-side rendering works like a WordPress page, where the HTML is just... there. It's not. If you've ever wondered why your React site tanks in search while a five-year-old WordPress blog outranks it with zero effort, you've run into the core issue in the React vs WordPress SEO debate. In this post I'll walk through why that gap exists and the concrete React SEO fixes that close it.
React vs WordPress SEO: Why WordPress Wins by Default
WordPress serves fully rendered HTML on the first request. The crawler hits your server, gets a complete <title>, <meta description>, headings, and body text. No JavaScript required. Nothing to wait for, nothing to execute.
React (and most SPA frameworks) ship an almost-empty HTML shell:
<!DOCTYPE html>
<html>
<head><title>React App</title></head>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>
Everything meaningful (your content, your meta tags, your headings) gets injected after JavaScript runs. Google's crawler can execute JS, but it does so in a second wave, sometimes days later, with a limited rendering budget. Other bots (Bing, LinkedIn, Slack previews, some AI crawlers) often don't execute JS at all. Result: your beautifully designed page shows up as a blank card everywhere except a browser.
This isn't a React flaw, it's a rendering-strategy mismatch. The fix is making sure critical SEO content exists in the initial HTML response, not just in the post-hydration DOM.
React SEO Fix #1: Server-Side Render or Statically Generate Your Critical Pages
If you're on plain Create React App with client-only rendering, the highest-leverage fix is moving to a framework that renders HTML on the server or at build time.
With Next.js, a blog post page can look like this:
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Result: view-source on this page now shows the actual <h1>, meta description, and Open Graph tags before a single line of client JS runs. Test it with curl:
curl -s https://yoursite.com/blog/your-post | grep -A1 "<title>"
If you see your real title instead of "React App," you're done with step one.
React SEO Fix #2: Don't Forget Structured Data (Schema.org)
Even with SSR, most React tutorials skip structured data entirely, which is a shame, because it's often the difference between a plain blue link and a rich result with star ratings, breadcrumbs, or FAQ dropdowns.
Here's a minimal, framework-agnostic way to inject JSON-LD:
function ArticleSchema({ post }) {
const schema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
datePublished: post.date,
author: { "@type": "Person", name: post.author },
image: post.coverImage,
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
Result: paste your page URL into Google's Rich Results Test and you'll see it parse the BlogPosting type correctly. This alone has gotten pages of mine into rich snippets within a couple of weeks.
Managing this by hand across dozens of pages gets tedious fast, which is why a lot of React teams reach for a small metadata layer instead of copy-pasting schema blocks everywhere. I've been using @power-seo for this. It wraps generateMetadata-style config and JSON-LD injection into one hook so you set title/description/schema once per route instead of scattering <script> tags across components:
import { usePowerSEO } from "@power-seo/react";
function BlogPost({ post }) {
usePowerSEO({
title: post.title,
description: post.excerpt,
schema: { type: "BlogPosting", ...post },
});
return <article>{/* content */}</article>;
}
It's not magic, under the hood it's doing the same <head> manipulation and JSON-LD injection shown above. It's just less to maintain once you have more than a handful of pages. Plain hand-rolled functions work fine too; this is a convenience, not a requirement.
React SEO Fix #3: Fix Your Sitemap and robots.txt (The Boring Part That Actually Matters)
WordPress auto-generates a sitemap. React apps often ship with none, or a stale static one nobody updates.
A quick dynamic sitemap in Next.js:
// app/sitemap.js
export default async function sitemap() {
const posts = await getAllPosts();
return posts.map((post) => ({
url: `https://yoursite.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: "weekly",
priority: 0.7,
}));
}
Result: https://yoursite.com/sitemap.xml now reflects reality, and Google Search Console's Coverage report stops showing "Discovered, currently not indexed" for pages that have existed for months. I've watched indexation jump from ~40% to ~90% of submitted URLs within three weeks of fixing just this. You can read a longer breakdown of this specific case at ccbd.dev if you want the full before/after data.
What I Learned About React vs WordPress SEO
- WordPress isn't "better" for SEO, it's just rendering HTML earlier in the pipeline. Match that, and React is on equal footing.
- SSR/SSG matters more than any meta tag trick. Fix the rendering strategy first; everything else is polish.
- Structured data is underused by React devs because the DX around it is worse than plain HTML templates, worth automating early.
- Sitemaps and robots.txt are boring but non-negotiable. Crawlers can't index what they can't find.
If you want to try this approach, here's the repo: https://github.com/CyberCraftBD/power-seo
Let's Talk
What's actually slowed you down more: getting React to render SEO-friendly HTML, or getting Google to actually crawl and index it once it does? Curious if others are hitting the indexing wall even after fixing SSR.
Top comments (0)