1Install the package

Pick your package manager and run one command:

npmshell
npm install react-ssr-seo-toolkit
yarnshell
yarn add react-ssr-seo-toolkit
pnpmshell
pnpm add react-ssr-seo-toolkit
i

Peer dependency: React >= 18.0.0 is required. Zero other dependencies.

2Recommended project structure

Here's a typical folder layout. The key idea: pages never write <html> or <head> tags — that's handled by the Document component, just like in Next.js or any modern React framework.

my-app/
├── config/
│ └── seo.ts # Site-wide SEO defaults
├── components/
│ └── Document.tsx # Handles <html>, <head>, <SEOHead>, <body>
├── pages/
│ ├── HomePage.tsx # Just content + SEO config
│ ├── AboutPage.tsx # No <html> tags here!
│ └── BlogPost.tsx
├── server.tsx # Express / SSR entry point
└── package.json
TIP

This is the same pattern used by Next.js (layout.tsx), Remix (root.tsx), and React Router (Root component). Your pages only return content — the Document handles the HTML shell.

3Create a shared SEO config

This file holds defaults that every page inherits. Pages override only what they need.

config/seo.tsTypeScript
import { createSEOConfig } from "react-ssr-seo-toolkit";

export const siteConfig = createSEOConfig({
  titleTemplate: "%s | My Site",
  description: "Default site description for SEO.",
  openGraph: {
    siteName: "My Site",
    type: "website",
    locale: "en_US",
  },
  twitter: {
    card: "summary_large_image",
    site: "@mysite",
  },
});

export const SITE_URL = "https://mysite.com";
TIP

titleTemplate uses %s as a placeholder. Setting title: "About" on a page renders as About | My Site.

3.5Create a Document component

The Document handles <html>, <head>, <SEOHead>, and <body> so your pages never have to. This is the same pattern used by Next.js layout.tsx, Remix root.tsx, and React Router's root component.

components/Document.tsxTSX
import { SEOHead, JsonLd } from "react-ssr-seo-toolkit";
import type { SEOConfig } from "react-ssr-seo-toolkit";

interface DocumentProps {
  children: React.ReactNode;
  seo: SEOConfig;
  schemas?: Record<string, unknown>[];
}

export function Document({ children, seo, schemas }: DocumentProps) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport"
          content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />

        {/* SEO tags are rendered here — pages don't need to */}
        <SEOHead {...seo} />

        {/* Render any JSON-LD schemas passed by the page */}
        {schemas?.map((schema, i) => (
          <JsonLd key={i} data={schema} />
        ))}
      </head>
      <body>
        <nav>{/* Your shared navigation */}</nav>
        <main>{children}</main>
        <footer>{/* Your shared footer */}</footer>
      </body>
    </html>
  );
}
i

Key concept: The Document is the only file that writes <html> and <head>. Every page just provides its SEO config and content — the Document renders everything else.

4Build your first SEO page

Notice: no <html> or <head> tags here! The page just provides its SEO config and content. The Document handles the rest.

pages/AboutPage.tsxTSX
import { mergeSEOConfig, buildCanonicalUrl } from "react-ssr-seo-toolkit";
import { siteConfig, SITE_URL } from "../config/seo";
import { Document } from "../components/Document";

export function AboutPage() {
  // Merge site defaults with page-specific SEO
  const seo = mergeSEOConfig(siteConfig, {
    title: "About Us",
    description: "Learn about our mission and team.",
    canonical: buildCanonicalUrl(SITE_URL, "/about"),
    openGraph: {
      title: "About Us — My Site",
      description: "Learn about our mission and team.",
      url: buildCanonicalUrl(SITE_URL, "/about"),
    },
  });

  // The Document renders <html>, <head>, <SEOHead>, and <body>
  // You only write your page content here
  return (
    <Document seo={seo}>
      <h1>About Us</h1>
      <p>Our story...</p>
    </Document>
  );
}

How it works together:

The Document receives your SEO config and renders the full HTML document. Here's what the final output looks like:

Final HTML outputHTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" href="/favicon.ico" />

    <!-- Generated by SEOHead via Document -->
    <title>About Us | My Site</title>
    <meta name="description" content="Learn about our mission and team." />
    <link rel="canonical" href="https://mysite.com/about" />
    <meta property="og:title" content="About Us — My Site" />
    <meta property="og:description" content="Learn about our mission and team." />
    <meta property="og:url" content="https://mysite.com/about" />
    <meta property="og:site_name" content="My Site" />
    <meta property="og:type" content="website" />
    <meta property="og:locale" content="en_US" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@mysite" />
  </head>
  <body>
    <nav><!-- shared navigation --></nav>
    <main>
      <h1>About Us</h1>
      <p>Our story...</p>
    </main>
    <footer><!-- shared footer --></footer>
  </body>
</html>

5Add JSON-LD structured data

Structured data helps Google show rich results (star ratings, FAQs, breadcrumbs, etc). Pass schemas to the Document — it renders them inside <head> for you:

pages/BlogPost.tsxTSX
import {
  mergeSEOConfig,
  buildCanonicalUrl,
  createArticleSchema,
  createBreadcrumbSchema,
} from "react-ssr-seo-toolkit";
import { siteConfig, SITE_URL } from "../config/seo";
import { Document } from "../components/Document";

export function BlogPost() {
  const seo = mergeSEOConfig(siteConfig, {
    title: "My Blog Post",
    description: "A short summary of the post.",
    canonical: buildCanonicalUrl(SITE_URL, "/blog/my-post"),
  });

  // Create structured data schemas
  const articleSchema = createArticleSchema({
    headline: "My Blog Post",
    url: buildCanonicalUrl(SITE_URL, "/blog/my-post"),
    datePublished: "2025-06-15",
    dateModified: "2025-07-01",
    author: [
      { name: "Jane Doe", url: "https://mysite.com/authors/jane" },
    ],
    publisher: {
      name: "My Site",
      logo: "https://mysite.com/logo.png",
    },
    images: ["https://mysite.com/images/post.jpg"],
  });

  const breadcrumb = createBreadcrumbSchema([
    { name: "Home", url: "https://mysite.com" },
    { name: "Blog", url: "https://mysite.com/blog" },
    { name: "My Blog Post", url: "https://mysite.com/blog/my-post" },
  ]);

  // Pass schemas to Document — it renders them in <head>
  return (
    <Document seo={seo} schemas={[articleSchema, breadcrumb]}>
      <article>
        <h1>My Blog Post</h1>
        <p>Post content here...</p>
      </article>
    </Document>
  );
}
TIP

Notice how the page never touches <html>, <head>, or <SEOHead> directly. It just defines what SEO data it needs — the Document handles where it goes.

6Framework integration

Every framework has a "layout" concept. The toolkit works with all of them. Choose your framework for a detailed guide:

Here's a quick overview of how the pattern maps to each framework:

FrameworkWhere <SEOHead> goesWhere JSON-LD goesGuide
Express SSRDocument component (<head>)Document component (<head>)React Guide
Next.js App RouterNot used — use generateMetadataPage body via safeJsonLdSerializeNext.js Guide
Next.js Pages RouterInside <Head> from next/headInside <Head> or page bodyNext.js Guide
React Router 7root.tsx layout (<head>)root.tsx layout (<head>)React Guide
Remixroot.tsx layout (<head>)root.tsx layout (<head>)React Guide
VIEW

This very page has SEO tags! Right-click and select "View Page Source" to inspect the meta tags generated by react-ssr-seo-toolkit.