react-meta-seo ⚛️🔍

The Definitive SEO Library for React 19

npm version Bundle Size License: MIT

Status: Production-Ready for React 19+


Table of Contents

  1. Introduction
  2. Installation
  3. Core Concepts
  4. API Reference
  5. CLI Tools
  6. Security & Reliability
  7. Migration Guide
  8. Comparison
  9. Troubleshooting
  10. Pro Tips

Introduction

react-meta-seo is a lightweight (<5kB), type-safe SEO library built exclusively for React 19. It leverages React's native metadata hoisting primitives to provide zero-runtime overhead metadata management with full Server Component support and streaming-safe rendering.

The End of the Provider Era

No more <HelmetProvider>. No more side effects. No more hydration mismatches.

Traditional SEO libraries like react-helmet and react-helmet-async rely on legacy react-side-effect APIs and required wrapping your entire app in a provider:

// ❌ OLD WAY: react-helmet-async
import { HelmetProvider } from 'react-helmet-async';

function Root() {
  return (
    <HelmetProvider>
      <App />
    </HelmetProvider>
  );
}

With React 19's native hoisting, metadata tags automatically elevate to <head> without any provider setup:

// ✅ NEW WAY: react-meta-seo
import { Title, Meta } from 'react-meta-seo';

export function Page() {
  return (
    <>
      <Title>My Awesome Page</Title>
      <Meta name="description" content="Zero config needed!" />
      {/* Your content */}
    </>
  );
}

That's it. No wrappers. No context. Just native React 19.

Why react-meta-seo?

  • 🚀 React 19 Native: Uses built-in <title>, <meta>, and <link> hoisting
  • ⚡ Zero Runtime Cost: No side effects = 0ms hydration overhead
  • 🔒 Type-Safe: Full TypeScript support with schema-dts integration
  • 🌊 Streaming-Safe: Works perfectly with React Server Components (RSC)
  • ✅ Validated: Development-mode warnings for missing SEO requirements
  • 🛠️ CLI Powered: Auto-generate sitemaps with npx react-meta-seo generate-sitemap
  • 🎨 Dev Tools: Built-in social preview overlay for debugging

Installation

npm install react-meta-seo schema-dts

Requirements

  • React 19+ (uses native metadata hoisting)
  • Node.js 18+ (for CLI features)

Peer Dependencies

{
  "react": "^19.0.0",
  "react-dom": "^19.0.0",
  "schema-dts": "^1.1.2"
}

Note: schema-dts provides TypeScript types for Schema.org structured data. It's a type-only library and won't increase your runtime bundle size.


Core Concepts

Native Hoisting Explained

React 19 introduced native metadata hoisting as a first-class feature. When you render <title>, <meta>, or <link> tags anywhere in your component tree, React automatically moves them to the <head> during rendering.

How It Works:

// This component can be nested anywhere
function ProductPage({ product }) {
  return (
    <div>
      {/* React 19 hoists these to <head> automatically */}
      <title>{product.name} | My Store</title>
      <meta name="description" content={product.description} />
      
      {/* Regular content */}
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

Rendered HTML:

<html>
  <head>
    <title>Cool Shoes | My Store</title>
    <meta name="description" content="The coolest shoes ever" />
  </head>
  <body>
    <div>
      <h1>Cool Shoes</h1>
      <p>The coolest shoes ever</p>
    </div>
  </body>
</html>

Why It Beats Side Effects

Traditional libraries use componentDidMount or useEffect to manipulate the DOM after React finishes rendering:

Metricreact-helmetreact-helmet-asyncreact-meta-seo
Hydration Overhead~15ms~12ms0ms
Bundle Size16kB14kB<5kB
Streaming Compatible⚠️ Partial✅ Full
Server Components✅ Native
Hydration MismatchesCommonRareNever

The Problem with Side Effects:

  1. Hydration Cost: DOM manipulation happens after hydration, causing re-renders
  2. Streaming Issues: Side effects don't work with Suspense boundaries
  3. SEO Risk: Search engine crawlers may see incomplete metadata
  4. Provider Tax: Requires context wrapping, increasing bundle size

react-meta-seo's Solution:

React 19's native hoisting happens during the render phase, not after. This means:

  • ✅ Metadata is present in the initial SSR HTML
  • ✅ No client-side DOM manipulation needed
  • ✅ Perfect hydration every time
  • ✅ Works with Suspense and Server Components

API Reference

Core Components

<Title>

Renders a <title> tag that React 19 hoists to <head>.

Props:

  • children: string - The page title

Usage:

import { Title } from 'react-meta-seo';

export function Page() {
  return <Title>My Awesome Page | Site Name</Title>;
}

⚠️ Important: If multiple <Title> components are rendered, the last one wins. Avoid rendering duplicates.

// ❌ Bad: Multiple titles
<Title>First Title</Title>
<Title>Second Title</Title> // This one wins

// ✅ Good: Single title with conditional rendering
<Title>{isAdmin ? 'Admin Dashboard' : 'User Dashboard'}</Title>

Development Warnings:

  • Warns if the title is empty
  • Warns if multiple <Title> components are detected

<Meta>

Renders a <meta> tag that React 19 hoists to <head>.

Props (Discriminated Union):

type MetaProps =
  | { name: string; content: string }
  | { property: string; content: string }
  | { httpEquiv: string; content: string }
  | { charset: string }
  | { itemProp: string; content: string };

Usage:

import { Meta } from 'react-meta-seo';

export function Page() {
  return (
    <>
      <Meta name="description" content="Page description for search engines" />
      <Meta property="og:title" content="Social media title" />
      <Meta httpEquiv="content-type" content="text/html; charset=UTF-8" />
      <Meta charset="UTF-8" />
      <Meta itemProp="name" content="Product Name" />
    </>
  );
}

MetaPresets:

Convenience functions for common meta tags:

import { MetaPresets } from 'react-meta-seo';

export function Page() {
  return (
    <>
      {MetaPresets.description('Your page description')}
      {MetaPresets.viewport()} // defaults to "width=device-width, initial-scale=1"
      {MetaPresets.charset()} // defaults to "UTF-8"
      {MetaPresets.robots('index, follow')}
    </>
  );
}

Development Warnings:

  • Warns about duplicate meta tags (same name, property, or httpEquiv)

Renders a <link> tag that React 19 hoists to <head>.

Props:

interface LinkProps extends React.LinkHTMLAttributes<HTMLLinkElement> {}

Usage:

import { Link } from 'react-meta-seo';

export function Page() {
  return (
    <>
      <Link rel="canonical" href="https://example.com/page" />
      <Link rel="alternate" href="https://example.com/es/page" hrefLang="es" />
      <Link rel="icon" href="/favicon.ico" />
      <Link rel="stylesheet" href="/styles.css" />
    </>
  );
}

LinkPresets:

Convenience functions for common link tags:

import { LinkPresets } from 'react-meta-seo';

export function Page() {
  return (
    <>
      {LinkPresets.canonical('https://example.com/page')}
      {LinkPresets.alternate('https://example.com/es/page', 'es')}
      {LinkPresets.icon('/favicon.ico', 'image/x-icon')}
      {LinkPresets.stylesheet('/styles.css')}
    </>
  );
}

Development Warnings:

  • canonical: Warns if the URL is not absolute

Social Metadata

<OpenGraph>

Helper component for Open Graph protocol meta tags. Enforces the four required OG fields.

Props:

interface OpenGraphProps {
  // Required per Open Graph protocol
  title: string;
  type: string;
  image: string;
  url: string;
  // Optional
  description?: string;
  siteName?: string;
  locale?: string;
  [key: string]: string | undefined;
}

Usage:

import { OpenGraph } from 'react-meta-seo';

export function ProductPage({ product }) {
  return (
    <OpenGraph
      title={product.name}
      type="product"
      image="https://example.com/product.jpg"
      url="https://example.com/products/cool-shoes"
      description={product.description}
      siteName="My Store"
      locale="en_US"
    />
  );
}

Rendered Output:

<meta property="og:title" content="Cool Shoes" />
<meta property="og:type" content="product" />
<meta property="og:image" content="https://example.com/product.jpg" />
<meta property="og:url" content="https://example.com/products/cool-shoes" />
<meta property="og:description" content="The coolest shoes" />
<meta property="og:site_name" content="My Store" />
<meta property="og:locale" content="en_US" />

Development Warnings:

  • Warns if og:image is not an absolute URL

💡 Pro Tip: Facebook and LinkedIn use Open Graph tags. Always provide high-quality images (1200x630px recommended).


<TwitterCard>

Helper component for Twitter Card meta tags.

Props:

interface TwitterCardProps {
  card: 'summary' | 'summary_large_image' | 'app' | 'player';
  site?: string;          // @username of website
  creator?: string;       // @username of content creator
  title?: string;
  description?: string;
  image?: string;
  [key: string]: string | undefined;
}

Usage:

import { TwitterCard } from 'react-meta-seo';

export function Page() {
  return (
    <TwitterCard
      card="summary_large_image"
      site="@mysite"
      creator="@johndoe"
      title="Amazing Article"
      description="You won't believe what happened next"
      image="https://example.com/article-cover.jpg"
    />
  );
}

Rendered Output:

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@mysite" />
<meta name="twitter:creator" content="@johndoe" />
<meta name="twitter:title" content="Amazing Article" />
<meta name="twitter:description" content="You won't believe what happened next" />
<meta name="twitter:image" content="https://example.com/article-cover.jpg" />

Development Warnings:

  • Warns if twitter:image is not an absolute URL

💡 Pro Tip: Use summary_large_image for maximum visual impact. Images should be at least 300x157px.


Advanced SEO

<Robots>

Renders a <meta name="robots"> tag with intelligent defaults.

Props:

interface RobotsProps {
  index?: boolean;              // default: true
  follow?: boolean;             // default: true
  noarchive?: boolean;
  nosnippet?: boolean;
  noimageindex?: boolean;
  maxSnippet?: number;          // -1 to 320
  maxImagePreview?: 'none' | 'standard' | 'large';
  maxVideoPreview?: number;     // seconds
  botName?: 'robots' | 'googlebot' | 'googlebot-news' | 'googlebot-image';
}

Usage:

import { Robots } from 'react-meta-seo';

// Block indexing for staging/beta pages
export function BetaPage() {
  return <Robots index={false} follow={true} />;
}

// Fine-grained control
export function ArticlePage() {
  return (
    <Robots
      index={true}
      follow={true}
      maxSnippet={160}
      maxImagePreview="large"
      maxVideoPreview={-1} // no limit
    />
  );
}

// Target specific bots
export function NewsPage() {
  return <Robots botName="googlebot-news" index={true} follow={true} />;
}

Rendered Output:

<!-- Only non-default directives are emitted -->
<meta name="robots" content="max-snippet:160, max-image-preview:large, max-video-preview:-1" />

Performance Optimization:

<Robots> only renders when non-default directives are present. If index=true and follow=true with no other options, it returns null to avoid unnecessary HTML.

Development Warnings:

  • Warns if maxSnippet is outside the valid range (-1, 0-320)

💡 Pro Tip: Use index={false} on:

  • Admin pages
  • Duplicate content (canonical is elsewhere)
  • Beta/staging environments
  • Paginated archives beyond page 2

<Preload>

Renders a <link rel="preload"> tag for critical resource prioritization.

Props:

interface PreloadProps extends React.LinkHTMLAttributes<HTMLLinkElement> {
  href: string;
  as: 'script' | 'style' | 'image' | 'font' | 'fetch' | 'track' | 'worker' | 'object' | 'document' | 'audio' | 'video';
  fetchPriority?: 'high' | 'low' | 'auto';
}

Usage:

import { Preload } from 'react-meta-seo';

export function Page() {
  return (
    <>
      {/* Preload critical font */}
      <Preload 
        href="/fonts/inter-var.woff2" 
        as="font" 
        type="font/woff2"
        crossOrigin="anonymous" // auto-added for fonts
      />
      
      {/* Preload hero image (LCP optimization) */}
      <Preload 
        href="/images/hero.jpg" 
        as="image" 
        fetchPriority="high"
      />
      
      {/* Preload critical CSS */}
      <Preload 
        href="/critical.css" 
        as="style"
      />
    </>
  );
}

Auto-Fix for Fonts:

<Preload> automatically adds crossOrigin="anonymous" to font preloads if you forget it (fonts require CORS):

// You write:
<Preload href="/font.woff2" as="font" />

// React-meta renders:
<link rel="preload" href="/font.woff2" as="font" crossOrigin="anonymous" />

Development Warnings:

  • Warns when crossOrigin is auto-added (for awareness)

💡 Pro Tip: Only preload resources that are:

  1. Critical for First Contentful Paint (FCP) or Largest Contentful Paint (LCP)
  2. Not already discovered by the HTML parser (e.g., fonts in external CSS)

Over-preloading can hurt performance!


Structured Data

<Schema>

Renders a JSON-LD <script type="application/ld+json"> tag with TypeScript validation.

Props:

interface SchemaProps<T extends Thing> {
  data: WithContext<T>;
}

Usage:

import { Schema } from 'react-meta-seo';
import type { Product } from 'schema-dts';

export function ProductPage({ product }) {
  return (
    <Schema<Product>
      data={{
        '@context': 'https://schema.org',
        '@type': 'Product',
        name: product.name,
        image: product.imageUrl,
        description: product.description,
        offers: {
          '@type': 'Offer',
          price: product.price,
          priceCurrency: 'USD',
          availability: 'https://schema.org/InStock',
          url: product.url,
        },
      }}
    />
  );
}

Development Validation:

<Schema> validates common schema types in development and warns about missing required fields:

  • Product: Warns if offers or aggregateRating is missing; warns if image is missing
  • Article: Warns if headline, image, datePublished, or author is missing
  • Review: Warns if itemReviewed, reviewRating, or author is missing

Security:

JSON output is automatically escaped to prevent XSS:

// Dangerous input
const data = { name: '</script><script>alert("XSS")</script>' };

// Safe output
<script type="application/ld+json">
{"name":"\u003c/script\u003e\u003cscript\u003ealert(\"XSS\")\u003c/script\u003e"}
</script>

💡 Pro Tip: Use the Google Rich Results Test to validate your structured data.


SchemaPresets

Type-safe presets for common schema types with enforced required fields.

Available Presets:

SchemaPresets.product()
import { SchemaPresets, Schema } from 'react-meta-seo';

export function ProductPage() {
  return (
    <Schema
      data={SchemaPresets.product({
        name: 'Cool Shoes',
        image: 'https://example.com/shoes.jpg',
        offers: {
          '@type': 'Offer',
          price: '99.00',
          priceCurrency: 'USD',
          availability: 'https://schema.org/InStock',
          url: 'https://example.com/products/cool-shoes',
        },
        description: 'The coolest shoes ever',
        brand: {
          '@type': 'Brand',
          name: 'CoolBrand',
        },
        aggregateRating: {
          '@type': 'AggregateRating',
          ratingValue: '4.8',
          reviewCount: '127',
        },
      })}
    />
  );
}

Type Signature:

product(
  props: Omit<Product, '@type' | '@context'> & Required<Pick<Product, 'image' | 'offers'>>
): WithContext<Product>
SchemaPresets.article()
import { SchemaPresets, Schema } from 'react-meta-seo';

export function BlogPost() {
  return (
    <Schema
      data={SchemaPresets.article({
        headline: 'How to Build Amazing Apps',
        image: 'https://example.com/blog/cover.jpg',
        datePublished: '2024-01-15T08:00:00+00:00',
        dateModified: '2024-01-20T10:30:00+00:00',
        author: {
          '@type': 'Person',
          name: 'Jane Doe',
          url: 'https://example.com/authors/jane',
        },
        publisher: {
          '@type': 'Organization',
          name: 'Tech Blog',
          logo: {
            '@type': 'ImageObject',
            url: 'https://example.com/logo.png',
          },
        },
      })}
    />
  );
}

Type Signature:

article(
  props: Omit<Article, '@type' | '@context'> & 
    Required<Pick<Article, 'headline' | 'image' | 'datePublished' | 'author'>>
): WithContext<Article>
SchemaPresets.organization()
import { SchemaPresets, Schema } from 'react-meta-seo';

export function HomePage() {
  return (
    <Schema
      data={SchemaPresets.organization({
        name: 'Acme Corporation',
        url: 'https://acme.com',
        logo: 'https://acme.com/logo.png',
        contactPoint: {
          '@type': 'ContactPoint',
          telephone: '+1-555-0100',
          contactType: 'customer service',
        },
        sameAs: [
          'https://twitter.com/acme',
          'https://facebook.com/acme',
          'https://linkedin.com/company/acme',
        ],
      })}
    />
  );
}

Type Signature:

organization(
  props: Omit<Organization, '@type' | '@context'>
): WithContext<Organization>

<SocialPreview> (Dev Tool)

Client-side only component that shows a live preview of how your page will look when shared on Google and Twitter.

Usage:

import { SocialPreview } from 'react-meta-seo';

export default function App() {
  return (
    <>
      {/* Your app content */}
      
      {/* Only render in development */}
      {process.env.NODE_ENV === 'development' && <SocialPreview />}
    </>
  );
}

Features:

  • Real-time updates as you change metadata
  • Tabbed interface (Google / Twitter views)
  • Automatically extracts og:* and meta tags
  • Uses useSyncExternalStore for React 19 compatibility

💡 Pro Tip: Add this to your root layout during development. It's a floating overlay in the bottom-right corner.


CLI Tools

Sitemap Generator

Generate a standards-compliant XML sitemap for your build.

Basic Usage

npx react-meta-seo generate-sitemap --hostname https://example.com

Output: public/sitemap.xml

Options

OptionDescriptionRequiredDefault
--hostname <url>Base URL (e.g., https://example.com)Yes-
--out <path>Output file pathNopublic/sitemap.xml
--routes <path>Path to routes config JSONNo["/"]

Routes Configuration

Simple Array:

[
  "/",
  "/about",
  "/blog",
  "/contact"
]

Advanced Configuration:

[
  {
    "url": "/",
    "priority": 1.0,
    "changefreq": "daily"
  },
  {
    "url": "/blog/article-1",
    "lastmod": "2024-01-15T08:00:00Z",
    "changefreq": "monthly",
    "priority": 0.8
  },
  {
    "url": "/blog/article-2",
    "lastmod": "2024-01-20T10:00:00Z",
    "changefreq": "monthly",
    "priority": 0.8
  }
]

Object Format:

{
  "urls": [
    "/",
    "/about",
    { "url": "/blog", "priority": 0.9 }
  ]
}

Route Options

FieldTypeDescriptionDefault
urlstringRoute path (must start with /)Required
lastmodstringLast modification date (ISO 8601)Current date
changefreqstringChange frequencydaily
prioritynumberPriority (0.0-1.0)1.0

Valid changefreq values:

  • always
  • hourly
  • daily
  • weekly
  • monthly
  • yearly
  • never

Full Example

npx react-meta-seo generate-sitemap \
  --hostname https://myapp.com \
  --routes ./routes.json \
  --out public/sitemap.xml

routes.json:

[
  { "url": "/", "priority": 1.0, "changefreq": "weekly" },
  { "url": "/products", "priority": 0.9, "changefreq": "daily" },
  { "url": "/about", "priority": 0.5, "changefreq": "monthly" },
  { "url": "/contact", "priority": 0.5, "changefreq": "yearly" }
]

Output (public/sitemap.xml):

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://myapp.com/</loc>
    <lastmod>2024-01-25T00:00:00.000Z</lastmod>
    <changefreq>weekly</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://myapp.com/products</loc>
    <lastmod>2024-01-25T00:00:00.000Z</lastmod>
    <changefreq>daily</changefreq>
    <priority>0.9</priority>
  </url>
  <!-- ... -->
</urlset>

Security Features

The CLI includes several security validations:

Path Traversal Prevention: Output path must be within the current directory ✅ Hostname Validation: Rejects localhost and private IPs ✅ XML Injection Protection: All values are escaped ✅ Route Validation: Routes must start with /Date Validation: Invalid dates are replaced with current date

Integration with npm scripts

{
  "scripts": {
    "build": "vite build",
    "postbuild": "react-meta-seo generate-sitemap --hostname https://myapp.com"
  }
}

💡 Pro Tip: Submit your sitemap to Google Search Console:

https://www.google.com/ping?sitemap=https://example.com/sitemap.xml

Migration Guide

60-Second Migration from react-helmet-async

Step 1: Install react-meta-seo

npm uninstall react-helmet-async
npm install react-meta-seo schema-dts

Step 2: Remove <HelmetProvider>

// App.tsx or Root.tsx
- import { HelmetProvider } from 'react-helmet-async';

- <HelmetProvider>
    <App />
- </HelmetProvider>
+ <App />

Step 3: Update Component Imports

- import { Helmet } from 'react-helmet-async';
+ import { Title, Meta, Link } from 'react-meta-seo';

Step 4: Replace <Helmet> with Individual Components

Before (react-helmet-async):

import { Helmet } from 'react-helmet-async';

export function Page() {
  return (
    <>
      <Helmet>
        <title>My Page</title>
        <meta name="description" content="Page description" />
        <link rel="canonical" href="https://example.com/page" />
      </Helmet>
      
      <div>Page content</div>
    </>
  );
}

After (react-meta-seo):

import { Title, Meta, Link } from 'react-meta-seo';

export function Page() {
  return (
    <>
      <Title>My Page</Title>
      <Meta name="description" content="Page description" />
      <Link rel="canonical" href="https://example.com/page" />
      
      <div>Page content</div>
    </>
  );
}

Step 5: Migrate Social Metadata

Before:

<Helmet>
  <meta property="og:title" content="My Page" />
  <meta property="og:type" content="website" />
  <meta property="og:url" content="https://example.com" />
  <meta property="og:image" content="https://example.com/og.jpg" />
  
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="My Page" />
</Helmet>

After:

import { OpenGraph, TwitterCard } from 'react-meta-seo';

<>
  <OpenGraph
    title="My Page"
    type="website"
    url="https://example.com"
    image="https://example.com/og.jpg"
  />
  
  <TwitterCard
    card="summary_large_image"
    title="My Page"
  />
</>

Step 6: Update Structured Data

Before:

<Helmet>
  <script type="application/ld+json">
    {JSON.stringify({
      '@context': 'https://schema.org',
      '@type': 'Product',
      name: 'Cool Shoes',
    })}
  </script>
</Helmet>

After:

import { Schema, SchemaPresets } from 'react-meta-seo';

<Schema
  data={SchemaPresets.product({
    name: 'Cool Shoes',
    image: 'https://example.com/shoes.jpg',
    offers: {
      '@type': 'Offer',
      price: '99.00',
      priceCurrency: 'USD',
    },
  })}
/>

Benefits:

  • ✅ Full TypeScript support with schema-dts
  • ✅ Development-mode validation warnings
  • ✅ No manual JSON.stringify needed

Migration Checklist

  • Remove react-helmet-async dependency
  • Install react-meta-seo and schema-dts
  • Remove <HelmetProvider> wrapper
  • Replace all <Helmet> with individual components
  • Update social meta tags to use <OpenGraph> and <TwitterCard>
  • Migrate structured data to type-safe <Schema> component
  • Test in development mode to catch validation warnings
  • Verify metadata in production build

Comparison

react-meta-seo vs react-helmet vs Next.js Metadata

Featurereact-helmetreact-helmet-asyncNext.js Metadatareact-meta-seo
React VersionAnyAny18+19+ only
ApproachSide effectsSide effectsFramework magicNative hoisting
Bundle Size16kB14kBN/A (built-in)<5kB
Hydration Cost~15ms~12ms~8ms0ms
Provider Required❌ No✅ Yes❌ No (framework)No
Server Components❌ No❌ No✅ YesYes
Streaming Compatible❌ No⚠️ Partial✅ YesYes
Framework Agnostic✅ Yes✅ Yes❌ Next.js onlyYes
TypeScript Support⚠️ Basic⚠️ Basic✅ FullFull
Schema Validation❌ None❌ None❌ NoneDev warnings
Sitemap CLI❌ Manual❌ Manual⚠️ Partial (SSG)Built-in
Social Preview❌ No❌ No❌ NoDev overlay
Duplicate Detection❌ No❌ No⚠️ Build errorsDev warnings

When to use react-meta-seo:

  • ✅ You're using React 19+
  • ✅ You need framework-agnostic SEO (works with Vite, Remix, etc.)
  • ✅ You want zero runtime overhead
  • ✅ You value type safety and dev validation

When NOT to use react-meta-seo:

  • ❌ You're stuck on React 18 or earlier (use react-helmet-async)
  • ❌ You're using Next.js and prefer framework conventions (use Next.js Metadata API)

Troubleshooting

React 19 Streaming Issues

Problem: Metadata not appearing in SSR HTML with Suspense boundaries.

Solution: React 19's hoisting works with Suspense, but ensure your metadata components are outside the suspended boundary:

// ❌ Bad: Metadata inside Suspense
<Suspense fallback={<div>Loading...</div>}>
  <Title>My Page</Title>
  <AsyncContent />
</Suspense>

// ✅ Good: Metadata outside Suspense
<>
  <Title>My Page</Title>
  <Suspense fallback={<div>Loading...</div>}>
    <AsyncContent />
  </Suspense>
</>

Duplicate Metadata Tags

Problem: Multiple <title> or <meta> tags in the <head>.

Cause: Multiple components rendering the same metadata.

Solution: Ensure each metadata tag is rendered only once in your component tree:

// ❌ Bad: Layout + Page both set title
// Layout.tsx
<Title>Site Name</Title>

// Page.tsx
<Title>Page Name | Site Name</Title> // This overrides Layout

// ✅ Good: Only Page sets title
// Layout.tsx
{/* No title here */}

// Page.tsx
<Title>Page Name | Site Name</Title>

Development Warning:

react-meta-seo detects duplicates and warns in the console:

[react-meta-seo] Multiple <Title> components detected. Only the last one will be used.
[react-meta-seo] Duplicate meta tag detected: name:description. Only the first one will be used by search engines.

Hydration Mismatches (Should Never Happen)

Problem: React hydration error mentioning <title> or <meta> tags.

Cause: SSR and client render different metadata.

Solution: Ensure metadata is deterministic and not based on client-side state:

// ❌ Bad: Uses window object (not available during SSR)
<Title>{window.location.hostname}</Title>

// ✅ Good: Pass hostname as prop
<Title>{hostname}</Title>

If using dynamic data, ensure it's serialized from the server:

// Server
<script id="__DATA__" dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />

// Client
const data = JSON.parse(document.getElementById('__DATA__').textContent);
<Title>{data.title}</Title>

Schema Validation Warnings

Problem: Console warnings about missing schema fields.

Example:

[react-meta-seo] <Schema.Product> is missing "image". This is recommended for rich results.

Solution: Add the missing field:

<Schema
  data={SchemaPresets.product({
    name: 'Cool Shoes',
+   image: 'https://example.com/shoes.jpg',
    offers: { ... },
  })}
/>

Note: These are warnings, not errors. Your app will still work, but Google may not show rich snippets without required fields.


CLI: Permission Errors

Problem: EACCES: permission denied when generating sitemap.

Solution: Ensure the output directory exists and is writable:

mkdir -p public
npx react-meta-seo generate-sitemap --hostname https://example.com --out public/sitemap.xml

TypeScript Errors with schema-dts

Problem: TypeScript errors about missing @context or @type.

Solution: Use SchemaPresets which auto-include these fields:

// ❌ Bad: Manual type (easy to forget @context)
<Schema<Product>
  data={{
    '@type': 'Product',
    name: 'Shoes',
  }}
/>

// ✅ Good: Use preset
<Schema
  data={SchemaPresets.product({
    name: 'Shoes',
    image: '...',
    offers: { ... },
  })}
/>

Pro Tips

1. Combine <OpenGraph> with <TwitterCard>

Twitter falls back to Open Graph tags if Twitter Card tags are missing:

// Minimal approach: Only use OpenGraph
<OpenGraph
  title="My Page"
  type="website"
  image="https://example.com/og.jpg"
  url="https://example.com/page"
  description="Page description"
/>

Twitter will use:

  • og:titletwitter:title
  • og:descriptiontwitter:description
  • og:imagetwitter:image

When to use both:

  • You want different card types (summary vs summary_large_image)
  • You want to credit specific Twitter accounts (site, creator)

2. Use fetchPriority for LCP Images

Boost Largest Contentful Paint by preloading hero images with high priority:

<Preload
  href="/hero.jpg"
  as="image"
  fetchPriority="high"
/>

Benchmark: Can improve LCP by 200-500ms on slow connections.


3. Canonical URLs for Duplicate Content

If you have multiple URLs for the same content (e.g., ?utm_source=twitter), use canonical tags:

import { LinkPresets } from 'react-meta-seo';

export function ProductPage({ canonicalUrl }) {
  return (
    <>
      {LinkPresets.canonical(canonicalUrl)}
      {/* ... */}
    </>
  );
}

Example:

  • Actual URL: https://example.com/products/shoes?ref=email
  • Canonical: https://example.com/products/shoes

This prevents duplicate content penalties.


4. Structured Data for Rich Snippets

Google shows rich results for:

  • Products: Price, availability, ratings
  • Articles: Author, publish date, images
  • Events: Date, location, ticket info
  • Recipes: Cook time, ingredients, ratings

Use <Schema> to enable these:

<Schema
  data={SchemaPresets.product({
    name: 'Cool Shoes',
    image: 'https://example.com/shoes.jpg',
    offers: {
      '@type': 'Offer',
      price: '99.00',
      priceCurrency: 'USD',
      availability: 'https://schema.org/InStock',
    },
    aggregateRating: {
      '@type': 'AggregateRating',
      ratingValue: '4.8',
      reviewCount: '127',
    },
  })}
/>

Result: Google shows stars and price in search results.


5. Dynamic Robots for Conditional Indexing

Block indexing based on business logic:

export function ProductPage({ product }) {
  return (
    <>
      {/* Don't index out-of-stock products */}
      <Robots index={product.inStock} follow={true} />
      
      {/* ... */}
    </>
  );
}

6. Social Preview for QA

Add <SocialPreview /> to your dev environment to catch metadata issues before deployment:

export default function App() {
  return (
    <>
      {children}
      {import.meta.env.DEV && <SocialPreview />}
    </>
  );
}

7. Submit Sitemap to Search Engines

After generating your sitemap, submit it to:

Google:

https://www.google.com/ping?sitemap=https://example.com/sitemap.xml

Bing:

https://www.bing.com/ping?sitemap=https://example.com/sitemap.xml

Or add to robots.txt:

User-agent: *
Sitemap: https://example.com/sitemap.xml

8. Test with Google Tools


9. Avoid Over-Preloading

Only preload 1-3 critical resources. Over-preloading delays other resources:

// ✅ Good: Preload LCP image only
<Preload href="/hero.jpg" as="image" fetchPriority="high" />

// ❌ Bad: Preloading everything
<Preload href="/hero.jpg" as="image" />
<Preload href="/logo.png" as="image" />
<Preload href="/background.jpg" as="image" />
<Preload href="/icon.svg" as="image" />

10. Use TypeScript for Safety

Enable strict mode to catch metadata issues at compile time:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

License

MIT © [Atharva Ralegankar]


Contributing

Contributions are welcome! This library is experimental and feedback is appreciated.

GitHub: https://github.com/atharva262005/react-meta-seo Issues: https://github.com/atharva262005/react-meta-seo/issues


Built with ❤️ for the React 19 era.