DEV Community

Cover image for The Best SEO npm Package for React in 2026 (Tested on a Real App)
Mitu Das
Mitu Das

Posted on • Originally published at ccbd.dev

The Best SEO npm Package for React in 2026 (Tested on a Real App)

Google couldn't see my React app, and I had no idea why. I checked robots.txt, resubmitted my sitemap, blamed my hosting provider. The actual bug: my <title> tag said "React App" on every page, and my meta descriptions didn't exist at all.

If you've searched "SEO npm package for React," you're probably staring at the same problem, or trying to pick between the half-dozen open source SEO tools that all claim to fix it. I tested four of them on the same codebase so you don't have to guess. Here's what actually works, with code you can copy right now, and where each tool falls short.

Why React Needs a Dedicated SEO npm Package in the First Place

Client-side rendered React apps ship an almost-empty index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/bundle.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Your title, headings, and content only appear after JavaScript executes. Per Google's own documentation on JavaScript SEO, Googlebot does render JS, but rendering happens as a second wave, isn't guaranteed on every crawl, and other bots (Bing, LinkedIn, Slack unfurls) frequently skip it entirely. That's why a plain React SPA can rank inconsistently even with great content.

Check your own app right now:

curl -s https://your-react-app.com | grep -i "<title>"
Enter fullscreen mode Exit fullscreen mode

If every route prints the same generic title, you've confirmed the bug. This is the exact problem every npm package in this article is trying to solve, just with different tradeoffs.

Option 1: react-helmet-async for Dynamic Meta Tags Per Route

This is the most widely adopted open source SEO tool in the React ecosystem, with over 1.5M weekly downloads on npm. It lets each route own its <head>.

npm install react-helmet-async
Enter fullscreen mode Exit fullscreen mode
// App.jsx
import { HelmetProvider } from 'react-helmet-async';

function App() {
  return (
    <HelmetProvider>
      <YourRoutes />
    </HelmetProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode
// BlogPost.jsx
import { Helmet } from 'react-helmet-async';

function BlogPost({ post }) {
  return (
    <>
      <Helmet>
        <title>{post.title} | My Blog</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:image" content={post.coverImage} />
      </Helmet>
      <article>{/* content */}</article>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tested result: inspecting <head> in dev tools confirms tags update correctly per route. But running the earlier curl test still returns the default title, because Helmet only updates the DOM after hydration. For crawlers that skip JS execution, this fix is invisible. That's the gap the next two tools address.

Option 2: Prerendering or SSR to Make the Fix Visible to Every Crawler

If you're on Next.js, Remix, or Astro, this comes free: the server sends complete HTML on first request. On plain Vite or CRA, prerender your static routes at build time instead:

// scripts/prerender.js
import puppeteer from 'puppeteer';
import fs from 'fs';

const routes = ['/', '/about', '/blog/my-first-post'];

const browser = await puppeteer.launch();
const page = await browser.newPage();

for (const route of routes) {
  await page.goto(`http://localhost:3000${route}`, { waitUntil: 'networkidle0' });
  const html = await page.content();
  fs.mkdirSync(`dist${route}`, { recursive: true });
  fs.writeFileSync(`dist${route}/index.html`, html);
}

await browser.close();
Enter fullscreen mode Exit fullscreen mode

Run this as a post-build step. Tested result: re-running the curl command against the prerendered output now returns the correct, route-specific title and meta tags, with no JS execution required. This is the step that actually fixes crawler visibility, not just the DOM.

Option 3: Structured Data, Comparing next-seo, react-schemaorg, and @power-seo

Titles and descriptions get you indexed. JSON-LD structured data gets you rich results: star ratings, breadcrumbs, FAQ dropdowns. Hand-writing schema.org JSON for every content type gets tedious, so most React SEO npm packages bundle a helper for it.

Package Handles meta tags Generates JSON-LD Sitemap CLI Weekly downloads
next-seo Yes Yes (manual shape) No ~500k
react-schemaorg No Yes (typed) No ~15k
@power-seo Yes Yes (generated from data) Yes Newer, growing

I tested @power-seo on a product page because it combines meta tags and schema generation in one call, which cut down the boilerplate next-seo + react-schemaorg together would've required:

npm install @power-seo
Enter fullscreen mode Exit fullscreen mode
import { SEOHead, generateSchema } from '@power-seo';

function ProductPage({ product }) {
  const schema = generateSchema('Product', {
    name: product.name,
    price: product.price,
    rating: product.rating,
    reviewCount: product.reviewCount,
  });

  return (
    <SEOHead
      title={`${product.name} | Store`}
      description={product.shortDescription}
      jsonLd={schema}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Tested result: pasting the rendered HTML into Google's Rich Results Testvalidated the Product schema with no errors. It also ships a CLI to generate sitemap.xml from your route config, which pairs naturally with the prerender step above. next-seo and react-schemaorg cover overlapping ground and are equally valid choices. Pick based on whether you want sitemap generation bundled in or prefer to keep concerns separate.

What I Learned Testing These Tools

  • Diagnose before picking a package. Run the curl test first. "No dynamic tags" and "no crawlable HTML at all" are different problems with different fixes.
  • No SEO npm package replaces SSR or prerendering. react-helmet-async, next-seo, and @[power-seo all update](https://www.npmjs.com/org/power-seo) the DOM, but none of them fix a blank shell for non-JS crawlers on their own.
  • Structured data is the most skipped, highest-ROI step. It's usually one JSON object away from rich search results, and validating it takes two minutes with Google's Rich Results Test.
  • "Best" depends on your stack, not hype. A meta-framework app needs different tooling than a plain Vite SPA. Match the package to the gap you actually have.

If you want to try this approach, here's the repo: https://ccbd.dev/blog/seo-npm-package-for-react-complete-guide

Let's Talk

Which open source SEO tool are you running in production? react-helmet-async, next-seo, prerendering, full SSR, or something else? What broke or surprised you when you tested it against real crawlers? Drop your setup in the comments.

Top comments (1)

Collapse
 
bhavin-allinonetools profile image
Bhavin Sheth

The curl check is a great practical tip. I’ve seen teams add meta tags and assume SEO is fixed, while the actual HTML response still contains the same generic title on every route.