I spent 6 hours debugging why Google Search Console showed zero impressions for a landing page I'd just shipped. The culprit? A missing <title> tag because a lazy useEffect was overwriting it with an empty string on hydration. The fix was 4 lines of code. The wasted Friday afternoon was entirely avoidable.
If you're building React apps and treating SEO as an afterthought, this article is for you. I'll show you how to wire a real SEO content checker into your workflow so you catch these issues before they ever reach production.
Why React Apps Have an SEO Blind Spot
Server-rendered frameworks like Next.js have improved things, but a huge chunk of React apps are still SPAs that rely on client-side rendering. Crawlers have gotten smarter, but they're not perfect and even in SSR setups, meta tags get silently dropped, duplicated, or overwritten by nested component trees.
The bugs are subtle:
- A
<Helmet>component deep in a route component overrides the parent's<title>
- Dynamic OG images point to
undefinedbecause the data hadn't loaded yet - Description tags hit 300+ characters because someone copy-pasted a paragraph
You don't notice these locally because you can read the page. Google notices and demotes you.
The fix isn't better discipline. It's automated checking.
Step 1: Audit Your Existing Pages With a Node Script
Before adding tooling, you need a baseline. Here's a dead-simple script that crawls your rendered HTML and checks for common SEO problems:
// scripts/seo-audit.ts
import { JSDOM } from "jsdom";
import fetch from "node-fetch";
interface SEOReport {
url: string;
title: string | null;
titleLength: number;
description: string | null;
descriptionLength: number;
issues: string[];
}
async function auditPage(url: string): Promise<SEOReport> {
const res = await fetch(url);
const html = await res.text();
const dom = new JSDOM(html);
const doc = dom.window.document;
const title = doc.querySelector("title")?.textContent ?? null;
const descEl = doc.querySelector('meta[name="description"]');
const description = descEl?.getAttribute("content") ?? null;
const issues: string[] = [];
if (!title) issues.push("Missing <title> tag");
else if (title.length < 30) issues.push(`Title too short (${title.length} chars)`);
else if (title.length > 60) issues.push(`Title too long (${title.length} chars)`);
if (!description) issues.push("Missing meta description");
else if (description.length < 70) issues.push(`Description too short (${description.length} chars)`);
else if (description.length > 160) issues.push(`Description too long (${description.length} chars)`);
if (!doc.querySelector('meta[property="og:image"]'))
issues.push("Missing og:image");
if (!doc.querySelector("h1"))
issues.push("No <h1> tag found");
return {
url,
title,
titleLength: title?.length ?? 0,
description,
descriptionLength: description?.length ?? 0,
issues,
};
}
async function main() {
const urls = [
"http://localhost:3000",
"http://localhost:3000/about",
"http://localhost:3000/blog",
];
for (const url of urls) {
const report = await auditPage(url);
console.log(`\n📄 ${report.url}`);
if (report.issues.length === 0) {
console.log(" ✅ All checks passed");
} else {
report.issues.forEach((issue) => console.log(` ❌ ${issue}`));
}
}
}
main();
Run it against your local dev server:
npx ts-node scripts/seo-audit.ts
Result: You get a per-page issue list in 10 seconds. This alone will surface embarrassing problems you've been shipping silently.
Step 2: Add a fits naturally. It's a TypeScript-first SEO content checker tool that runs programmatically against your rendered output no headless browser required. I started using it after the setup above got repetitive across three client projects.
CODE npm install @power-seo
CODE // scripts/check-seo.ts
import { checkSEO } from "@power-seo";
const result = await checkSEO({
html: await fetch("http://localhost:3000/blog/my-post").then((r) => r.text()),
url: "https://yourdomain.com/blog/my-post",
});
if (!result.pass) {
console.error("SEO issues found:");
result.issues.forEach((issue) => {
console.error(` [${issue.severity}] ${issue.message}`);
});
process.exit(1); // Fail the CI build
}
npm install @power-seo
// scripts/check-seo.ts
import { checkSEO } from "@power-seo";
const result = await checkSEO({
html: await fetch("http://localhost:3000/blog/my-post").then((r) => r.text()),
url: "https://yourdomain.com/blog/my-post",
});
if (!result.pass) {
console.error("SEO issues found:");
result.issues.forEach((issue) => {
console.error(` [${issue.severity}] ${issue.message}`);
});
process.exit(1); // Fail the CI build
}
What I like about it: the severity field. Not every SEO issue is a blocker a missing OG image is a warning; a missing <title> is a critical error. That distinction matters when you're wiring this into a CI gate and you don't want to block deploys over minor things. The deeper write-up of how it handles edge cases (duplicate canonicals, hreflang conflicts, structured data validation) is on ccbd.dev.
What I Actually Learned
SEO bugs are invisible until they're expensive. Add the debug overlay from day one it costs you 20 minutes to set up and saves hours of Googling "why isn't my page indexed."
Test rendered HTML, not your JSX. Component props look fine. The DOM at runtime is where things break. Always audit the actual output.
The React SEO checklist you need is short. Title (30–60), description (70–160), one
<h1>, OG image, canonical. Everything else is optimization. Get these five right first.Fail builds on critical SEO issues. Treat a missing
<title>like a missingreturnstatement. It's a bug.
What's your SEO workflow?
Do you check meta tags manually, use a browser extension, or do you have something automated? I'm curious whether anyone has integrated SEO auditing into their preview deployment pipeline (Vercel/Netlify preview URLs specifically) that's the next thing I'm trying to solve. Drop your setup in the comments.
SOCIAL SHARE CARD GENERATOR