The pattern

In a real application, you have many different page types — home, listing, detail, dashboard, etc. Each needs different SEO data. The toolkit makes this easy with a three-layer approach:

LayerWhat it doesExample
1. Site configDefaults inherited by every pageTitle template, site name, Twitter handle
2. Page configOverrides for a specific pageTitle, description, canonical URL, OG image
3. Structured dataJSON-LD schemas for rich resultsArticle, Product, FAQ, Breadcrumb

Example: E-commerce application

Here's a complete multi-page e-commerce app. All pages use the same shared config.

1Shared config (used by all pages)

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

export const siteConfig = createSEOConfig({
  titleTemplate: "%s | ShopMax",
  description: "Shop the best products online at ShopMax.",
  openGraph: {
    siteName: "ShopMax",
    type: "website",
    locale: "en_US",
  },
  twitter: {
    card: "summary_large_image",
    site: "@shopmax",
  },
});

export const SITE_URL = "https://shopmax.com";

2Home page

The home page uses Organization and WebSite schemas for brand presence in search.

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

export function HomePage() {
  const seo = mergeSEOConfig(siteConfig, {
    title: "Home",
    canonical: SITE_URL,
    openGraph: {
      title: "ShopMax — Shop the Best Products Online",
      url: SITE_URL,
      images: [{ url: `${SITE_URL}/og-home.jpg`, width: 1200, height: 630 }],
    },
  });

  const schemas = [
    createOrganizationSchema({
      name: "ShopMax",
      url: SITE_URL,
      logo: `${SITE_URL}/logo.png`,
      sameAs: [
        "https://twitter.com/shopmax",
        "https://facebook.com/shopmax",
      ],
    }),
    createWebsiteSchema({
      name: "ShopMax",
      url: SITE_URL,
      description: "Shop the best products online.",
      searchUrl: `${SITE_URL}/search?q={search_term_string}`,
    }),
  ];

  return (
    <Document seo={seo} schemas={schemas}>
      <h1>Welcome to ShopMax</h1>
      <p>Featured products, promotions, etc.</p>
    </Document>
  );
}

3Category / listing page

Listing pages show a collection of items with breadcrumb navigation.

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

export function CategoryPage({ category }) {
  const url = buildCanonicalUrl(SITE_URL, `/category/${category.slug}`);

  const seo = mergeSEOConfig(siteConfig, {
    title: category.name,
    description: `Browse ${category.productCount} ${category.name} products.`,
    canonical: url,
    openGraph: {
      title: `${category.name} — ShopMax`,
      description: category.description,
      url,
    },
  });

  const schemas = [
    createBreadcrumbSchema([
      { name: "Home", url: SITE_URL },
      { name: category.name, url },
    ]),
  ];

  return (
    <Document seo={seo} schemas={schemas}>
      <h1>{category.name}</h1>
      <p>{category.productCount} products</p>
      {/* Product grid */}
    </Document>
  );
}

4Product detail page

Product pages need the richest SEO — product schema with pricing, ratings, availability, plus breadcrumbs.

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

export function ProductPage({ product }) {
  const url = buildCanonicalUrl(SITE_URL, `/product/${product.slug}`);

  const seo = mergeSEOConfig(siteConfig, {
    title: product.name,
    description: product.description,
    canonical: url,
    openGraph: {
      title: product.name,
      description: product.description,
      url,
      type: "product",
      images: [{ url: product.image, width: 1200, height: 630 }],
    },
    twitter: {
      title: product.name,
      description: `$${product.price} — ${product.description}`,
      image: product.image,
    },
  });

  const schemas = [
    createProductSchema({
      name: product.name,
      url,
      description: product.description,
      images: [product.image],
      price: product.price,
      priceCurrency: product.currency,
      sku: product.sku,
      brand: "ShopMax",
      availability: product.inStock
        ? "https://schema.org/InStock"
        : "https://schema.org/OutOfStock",
      ratingValue: product.rating,
      reviewCount: product.reviewCount,
    }),
    createBreadcrumbSchema([
      { name: "Home", url: SITE_URL },
      { name: product.category.name, url: buildCanonicalUrl(SITE_URL, `/category/${product.category.slug}`) },
      { name: product.name, url },
    ]),
  ];

  return (
    <Document seo={seo} schemas={schemas}>
      <h1>{product.name}</h1>
      <p>${product.price} {product.currency}</p>
      <p>{product.description}</p>
    </Document>
  );
}

5Blog / article page

pages/BlogPostPage.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 BlogPostPage({ post }) {
  const url = buildCanonicalUrl(SITE_URL, `/blog/${post.slug}`);

  const seo = mergeSEOConfig(siteConfig, {
    title: post.title,
    description: post.excerpt,
    canonical: url,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url,
      type: "article",
      images: [{ url: post.image, width: 1200, height: 630 }],
    },
  });

  const schemas = [
    createArticleSchema({
      headline: post.title,
      url,
      datePublished: post.date,
      dateModified: post.updatedDate,
      author: [{ name: post.author.name, url: post.author.url }],
      publisher: { name: "ShopMax Blog", logo: `${SITE_URL}/logo.png` },
      images: [post.image],
    }),
    createBreadcrumbSchema([
      { name: "Home", url: SITE_URL },
      { name: "Blog", url: buildCanonicalUrl(SITE_URL, "/blog") },
      { name: post.title, url },
    ]),
  ];

  return (
    <Document seo={seo} schemas={schemas}>
      <article>
        <h1>{post.title}</h1>
        <p>By {post.author.name} on {post.date}</p>
        <div>{post.content}</div>
      </article>
    </Document>
  );
}

6FAQ page

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

const faqItems = [
  { question: "What is your return policy?", answer: "30-day money-back guarantee." },
  { question: "How long does shipping take?", answer: "5-7 business days standard." },
  { question: "Do you ship internationally?", answer: "Yes, to 50+ countries." },
];

export function FAQHelpPage() {
  const seo = mergeSEOConfig(siteConfig, {
    title: "FAQ",
    description: "Answers to common questions about shipping, returns, and more.",
    canonical: buildCanonicalUrl(SITE_URL, "/faq"),
  });

  return (
    <Document seo={seo} schemas={[createFAQSchema(faqItems)]}>
      <h1>Frequently Asked Questions</h1>
      {faqItems.map((item, i) => (
        <div key={i}>
          <h3>{item.question}</h3>
          <p>{item.answer}</p>
        </div>
      ))}
    </Document>
  );
}

7Dashboard page (noindex)

Internal pages like dashboards should not appear in search results.

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

export function DashboardPage() {
  const seo = mergeSEOConfig(siteConfig, {
    title: "Dashboard",
    description: "Your account dashboard.",
    canonical: buildCanonicalUrl(SITE_URL, "/dashboard"),
    robots: noIndex(), // { index: false, follow: true }
  });

  return (
    <Document seo={seo}>
      <h1>Dashboard</h1>
      <p>Welcome back! Here are your stats.</p>
    </Document>
  );
}
TIP

noIndex() returns { index: false, follow: true }. For fully private pages, use noIndexNoFollow() instead.

Reusable SEO helpers

As your app grows, extract repeated patterns into helpers:

utils/seo-helpers.tsTypeScript
import {
  mergeSEOConfig,
  buildCanonicalUrl,
  createBreadcrumbSchema,
} from "react-ssr-seo-toolkit";
import { siteConfig, SITE_URL } from "../config/seo";
import type { SEOConfig } from "react-ssr-seo-toolkit";

/** Creates page SEO config with canonical URL */
export function createPageSEO(
  path: string,
  overrides: Partial<SEOConfig>
) {
  return mergeSEOConfig(siteConfig, {
    ...overrides,
    canonical: buildCanonicalUrl(SITE_URL, path),
    openGraph: {
      ...overrides.openGraph,
      url: buildCanonicalUrl(SITE_URL, path),
    },
  });
}

/** Creates breadcrumb trail (Home is automatic) */
export function createBreadcrumbs(
  items: Array<{ name: string; path: string }>
) {
  return createBreadcrumbSchema([
    { name: "Home", url: SITE_URL },
    ...items.map((item) => ({
      name: item.name,
      url: buildCanonicalUrl(SITE_URL, item.path),
    })),
  ]);
}

Now pages become much simpler:

pages/AboutPage.tsx (using helpers)TSX
import { createPageSEO, createBreadcrumbs } from "../utils/seo-helpers";
import { Document } from "../components/Document";

export function AboutPage() {
  const seo = createPageSEO("/about", {
    title: "About Us",
    description: "Learn about our team.",
  });

  return (
    <Document seo={seo} schemas={[createBreadcrumbs([{ name: "About", path: "/about" }])]}>
      <h1>About Us</h1>
      <p>Our story...</p>
    </Document>
  );
}

Multi-language pages

For multi-language sites, use alternates for hreflang links:

Hreflang exampleTSX
const seo = mergeSEOConfig(siteConfig, {
  title: "Home",
  canonical: SITE_URL,
  alternates: [
    { hreflang: "en", href: SITE_URL },
    { hreflang: "es", href: `${SITE_URL}/es` },
    { hreflang: "fr", href: `${SITE_URL}/fr` },
    { hreflang: "x-default", href: SITE_URL },
  ],
});
// Renders: <link rel="alternate" hreflang="en" href="..." />
//          <link rel="alternate" hreflang="es" href="..." /> etc.

Key takeaways

  • Every page uses mergeSEOConfig(siteConfig, pageOverrides)
  • Site defaults are inherited automatically — pages only override what they need
  • JSON-LD schemas are created per page and passed to the Document
  • Use noIndex() for private pages like dashboards
  • Extract reusable helpers as your app grows
  • Only the Document writes <html> and <head>