DEV Community

Cover image for I Built an SEO Content Checker Into My React App Here's the Exact Setup
Mitu Das
Mitu Das

Posted on • Originally published at ccbd.dev

I Built an SEO Content Checker Into My React App Here's the Exact Setup

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 undefined because 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();
Enter fullscreen mode Exit fullscreen mode

Run it against your local dev server:

npx ts-node scripts/seo-audit.ts
Enter fullscreen mode Exit fullscreen mode

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 React SEO Checklist as a Dev-Mode Overlay

The audit script is great for CI, but during development you want instant feedback. Here's a lightweight component that renders an SEO checklist overlay in dev mode no page reload required.

// components/SEODebugOverlay.tsx
import { useEffect, useState } from "react";

interface Check {
  label: string;
  pass: boolean;
  detail?: string;
}

function runChecks(): Check[] {
  const title = document.title;
  const descEl = document.querySelector<HTMLMetaElement>(
    'meta[name="description"]'
  );
  const desc = descEl?.content ?? "";
  const ogImage = document.querySelector('meta[property="og:image"]');
  const h1s = document.querySelectorAll("h1");
  const canonical = document.querySelector('link[rel="canonical"]');

  return [
    {
      label: "Title length (30–60 chars)",
      pass: title.length >= 30 && title.length <= 60,
      detail: `${title.length} chars: "${title.slice(0, 40)}…"`,
    },
    {
      label: "Meta description (70–160 chars)",
      pass: desc.length >= 70 && desc.length <= 160,
      detail: desc ? `${desc.length} chars` : "Missing",
    },
    {
      label: "OG image present",
      pass: !!ogImage,
    },
    {
      label: "Exactly one <h1>",
      pass: h1s.length === 1,
      detail: `Found ${h1s.length}`,
    },
    {
      label: "Canonical URL set",
      pass: !!canonical,
    },
  ];
}

export function SEODebugOverlay() {
  const [checks, setChecks] = useState<Check[]>([]);
  const [open, setOpen] = useState(false);

  useEffect(() => {
    setChecks(runChecks());
  }, []);

  if (process.env.NODE_ENV !== "development") return null;

  const failCount = checks.filter((c) => !c.pass).length;

  return (
    <div style={{ position: "fixed", bottom: 16, right: 16, zIndex: 9999 }}>
      <button
        onClick={() => setOpen((o) => !o)}
        style={{
          background: failCount > 0 ? "#ef4444" : "#22c55e",
          color: "#fff",
          border: "none",
          borderRadius: 8,
          padding: "6px 12px",
          cursor: "pointer",
          fontFamily: "monospace",
          fontSize: 13,
        }}
      >
        SEO {failCount > 0 ? `⚠ ${failCount} issues` : "āœ“"}
      </button>

      {open && (
        <div
          style={{
            background: "#1e1e2e",
            color: "#cdd6f4",
            borderRadius: 8,
            padding: 16,
            marginTop: 8,
            width: 320,
            fontFamily: "monospace",
            fontSize: 12,
          }}
        >
          {checks.map((c) => (
            <div key={c.label} style={{ marginBottom: 8 }}>
              <span style={{ color: c.pass ? "#a6e3a1" : "#f38ba8" }}>
                {c.pass ? "āœ“" : "āœ—"}
              </span>{" "}
              {c.label}
              {c.detail && (
                <div style={{ color: "#6c7086", marginLeft: 16 }}>
                  {c.detail}
                </div>
              )}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Drop <SEODebugOverlay /> into your root layout. It shows a green/red badge in the corner click it to see the full React SEO checklist without leaving the page.

Step 3: Pull It Into CI With a Proper Checker Package

The overlay and script are solid for most projects. When you're managing multiple routes or content-heavy pages (think: blogs, product pages, landing page variants), you want something more structured.

This is where @power-seo 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.

npm install @power-seo
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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 missing return statement. 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.

Top comments (0)