Multi-Page Application Usage
How to manage SEO across an entire application with multiple page types, shared configuration, and reusable patterns.
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:
| Layer | What it does | Example |
|---|---|---|
| 1. Site config | Defaults inherited by every page | Title template, site name, Twitter handle |
| 2. Page config | Overrides for a specific page | Title, description, canonical URL, OG image |
| 3. Structured data | JSON-LD schemas for rich results | Article, 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)
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.
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.
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.
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
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
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.
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>
);
}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:
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:
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:
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>