DEV Community

Alamin
Alamin

Posted on • Originally published at ccbd.dev

Your React App's SEO Is Broken and react-helmet Can't Fix It Anymore

react-helmet vs power-seo react: old error-prone SEO approach versus a modern, optimized, type-safe React SEO solution.
I spent 3 hours debugging why Google couldn't see my React app's meta tags. The fix was 4 lines of code. But the real lesson wasn't the fix, it was understanding why the library I'd trusted for years quietly stopped working with modern React. If you're still using react-helmet in a React 18 or 19 project, this is worth 10 minutes of your time.

The Problem Nobody Warned Me About

Here's what happened. I had a Vite + React 18 project. I installed react-helmet like I always do, wrote my tags, and deployed. Everything looked fine locally.

Then I ran npx lighthouse against the live URL. Missing meta description. Missing Open Graph image. Google Search Console confirmed it, Googlebot was crawling blank

tags.

The culprit? React-helmet was built before React 18's concurrent rendering model existed.

Its internal DOM manipulation layer races against React's reconciler in SSR and concurrent mode, causing hydration mismatches. The tags render on the client after the crawler has already read the page and moved on.
Here's the code I had — totally standard, nothing unusual:

import { Helmet } from 'react-helmet';

export function ProductPage({ product }) {
  return (
    <>
      <Helmet>
        <title>{product.name} | My Store</title>
        <meta name="description" content={product.summary} />
        <meta name="robots" content="index, follow, max-image-preview:large" />
        <meta property="og:image" content={product.image} />
      </Helmet>
      <main>...</main>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Looks fine. Ships fine. Breaks silently in production.
The react-helmet repo hasn't had a meaningful commit since 2022. The maintainers moved on. React didn't wait.

Why This Is Worse Than It Sounds

The hydration issue is one problem. But there's a second one that's arguably more dangerous: react-helmet accepts raw strings for everything.
Look at this robots tag:

<meta name="robots" content="index, follow, max-image-preveiw:large" />
Enter fullscreen mode Exit fullscreen mode

Spot the typo? preveiw instead of preview — two characters transposed. TypeScript doesn't catch it. Your linter doesn't catch it. It builds clean, ships to production, and quietly tanks your image preview rich results. I've seen this exact typo sitting in production codebases for months.
There's no autocomplete, no type checking, no enum, just a raw string that Google either understands or silently ignores.
And there's a third problem: no app-level defaults. Every page in your React app has to manage its full tag set from scratch. Forget one og:image on one route and you have an inconsistency that only shows up in a Search Console audit three months later.

What React 19 Actually Does to Head Management

React 19 changed something fundamental: <title>, <meta>, and <link> elements now hoist to <head> natively without any third-party DOM manipulation. This is a big deal.
React 19 docs on this:

React will now automatically hoist title, meta, and link tags to the document head when they're rendered anywhere in the tree.

React-helmet's whole value proposition was doing this manually via its own DOM layer. In React 19, that layer now conflicts with what React is doing natively, resulting in duplicate tags, race conditions, and unpredictable render order.
If you want to see this in action, spin up a React 19 + Vite project, install react-helmet, enable SSR, and watch the hydration warnings stack up in the console.

How I Fixed It (And What I Replaced It With)

After debugging, I tried a few options. next-seo is excellent, but it's locked to Next.js, and this was a Vite project. I needed something that:

  • Works with Vite and React 18/19
  • Has TypeScript-first APIs (no raw strings)
  • Supports structured data and Open Graph without boilerplate

I landed on Power-SEO React, a newer library from CyberCraft Bangladesh that's built around React 19's native head hoisting. Here's the same product page, rewritten:

import { SEO, Robots } from '@power-seo/react';

export function ProductPage({ product }) {
  return (
    <>
      <SEO
        title={product.name}
        description={product.summary}
        canonical={`https://mystore.com/products/${product.slug}`}
        openGraph={{
          type: 'website',
          images: [{ url: product.image, width: 1200, height: 630, alt: product.name }],
        }}
        twitter={{ cardType: 'summary_large_image' }}
      />
      <Robots index={true} follow={true} maxImagePreview="large" maxSnippet={160} />
      <main>...</main>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

maxImagePreview is typed as 'none' | 'standard' | 'large'. TypeScript literally cannot compile a typo. That one change eliminates the entire category of silent robots-string bugs.
But the real power is at the app level. The app-level setup with DefaultSEO is where it gets genuinely useful:

// _app.tsx — set defaults once, all pages inherit them
import { DefaultSEO } from '@power-seo/react';

export function App({ children }) {
  return (
    <DefaultSEO
      titleTemplate="%s | My Store"
      defaultTitle="My Store"
      description="Premium products for modern teams."
      openGraph={{
        type: 'website',
        siteName: 'My Store',
        images: [{ url: 'https://mystore.com/og-default.jpg', width: 1200, height: 630 }],
      }}
      twitter={{ site: '@mystore', cardType: 'summary_large_image' }}
    >
      {children}
    </DefaultSEO>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now every page inherits the default OG image, site name, and Twitter card. Per-page components only override what's different. If you forget an image on a product page, it falls back to the default, no blank OG tags.
One more thing that saved me on a multi-language project: built-in hreflang support.

import { Hreflang } from '@power-seo/react';

<Hreflang
  alternates={[
    { hrefLang: 'en', href: 'https://mystore.com/en/products/shoes' },
    { hrefLang: 'fr', href: 'https://mystore.com/fr/products/shoes' },
    { hrefLang: 'de', href: 'https://mystore.com/de/products/shoes' },
  ]}
  xDefault="https://mystore.com/en/products/shoes"
/>
Enter fullscreen mode Exit fullscreen mode

With react-helmet, I was writing 4 raw tags per page per locale. Across 200 product pages × 3 locales, that's 600+ places for one URL typo to create an hreflang conflict Google silently ignores.

What I Learned

React-helmet is not broken by itself, it's broken by the ecosystem moving around it. React 18 concurrent mode and React 19 native head hoisting changed the rules. React-helmet wasn't updated to match.

Raw string props in SEO libraries are a liability, not a convenience. The typo you can't see is the bug you won't find until it's already costing you organic traffic.

App-level SEO defaults are underrated. The pattern of setting global defaults once and overriding per page scales massively better than every page managing its own complete tag set.

Library maintenance matters for infrastructure dependencies. SEO libraries touch every page of your app. A library that hasn't tracked React's major versions is technical debt waiting to surface.

If you want to explore this approach yourself, the source is here: GitHub Power-SEO Repo

For the full comparison — feature table, migration guide, and more real-world scenarios — read the complete article: Power-SEO React vs React-Helmet

Top comments (0)