A composable, React-native SEO toolkit. Two APIs - a compound component tree and a hook - both built on the same framework-agnostic tag registry, so behavior never drifts between them.
Inspired by astro-seo's feature set (title templates, Open Graph, Twitter cards, canonical/robots, language alternates), but designed around React's composition model instead of a single flat props object - which is the right shape for an Astro template, not for a React render tree.
npm install react-seo
Requires React 19+ for automatic hoisting of <title>, , , and <script> rendered anywhere in the tree. On React 18 or SSR setups without hoisting, use the optional react-seo/helmet adapter (requires react-helmet-async as well).
import { Seo, SeoDefaultsProvider } from "react-seo";
// once, at the app root
<SeoDefaultsProvider
defaults={{
titleTemplate: "%s | My Site",
openGraph: { siteName: "My Site" },
twitter: { site: "@mysite" },
}}
>
<App />
</SeoDefaultsProvider>;
// per page — only what differs from the defaults
<Seo canonical="https://example.com/about">
<Seo.Title>About</Seo.Title>
<Seo.Description>Learn what we do and why.</Seo.Description>
<Seo.OpenGraph type="article" image="/cover.png" />
<Seo.Twitter card="summary_large_image" creator="@you" />
<Seo.JsonLd data={{ "@context": "https://schema.org", "@type": "AboutPage", name: "About" }} />
<Seo.Meta name="theme-color" content="#0b0b0b" />
<Seo.Link rel="icon" href="/favicon.ico" />
</Seo>;
Provider defaults merge field-by-field with page-level values — a page setting openGraph={{ type: "article" }} won't wipe out the provider's openGraph.siteName.
Same input shape, for use in loaders or anywhere outside JSX:
import { useSeo } from "react-seo";
const tags = useSeo({
title: "About",
openGraph: { type: "article" },
});
// tags.title, tags.meta, tags.link, tags.script
Next owns itself via generateMetadata, so use the adapter instead of :
import { toNextMetadata } from "react-seo/nextjs";
export const metadata = toNextMetadata({
title: "About",
openGraph: { type: "article", images: [{ url: "/cover.png" }] },
});
In non-production builds, both APIs warn (not throw) on common mistakes: missing title/description, og:image with no alt, an Open Graph block with no images, and <Seo.Title> rendered twice in the same tree.
index.ts # public entry point, re-exports everything below
src/
└── core/ # framework-agnostic tag model + builders, shared by both APIs
merged/ # the two APIs that build on core/, grouped together
├── compound/ # <Seo> / <Seo.Title> / <Seo.OpenGraph> / ...
└── hook/ # useSeo()
descriptors/ # things that describe environment/config, not SEO data itself
├── adapters/ # nextMetadata.ts, helmet.tsx
└── provider/ # SeoDefaultsProvider
framework-agnostic/ # reserved for non-React bindings (Vue/Svelte/etc.), not yet implemented
tests/
examples/
npm run build # tsup — esm + cjs + .d.ts
npm run dev # tsup --watch
npm test # vitest run
npm run typecheck # tsc --noEmit