Next.js Integration Guide
How to use react-ssr-seo-toolkit with Next.js App Router and Pages Router. Covers metadata, JSON-LD, dynamic routes, and multi-page setup.
1How Next.js SEO differs
Next.js has its own metadata system. Instead of rendering <SEOHead>in a layout, you use Next.js's generateMetadata function combined with the toolkit's builder functions. Here's what to use where:
| What | Where | How |
|---|---|---|
| Title, description, OG, Twitter | generateMetadata() in each page | Use mergeSEOConfig, buildTitle, buildCanonicalUrl |
| JSON-LD structured data | Page component body | Use schema generators + safeJsonLdSerialize |
| Robots directives | generateMetadata() | Use noIndex(), buildRobotsDirectives() |
Do not use <SEOHead> in Next.js App Router pages. Next.js manages <head> through generateMetadata. Use the toolkit's builder functions to generate the metadata values, then return them in Next.js's format.
Next.js App Router
The App Router (Next.js 13+) is the recommended approach.
2Project structure (App Router)
├── app/
│ ├── layout.tsx # Root layout (Next.js handles <html>)
│ ├── page.tsx # Home page
│ ├── about/
│ │ └── page.tsx # About page
│ ├── blog/
│ │ ├── page.tsx # Blog listing page
│ │ └── [slug]/
│ │ └── page.tsx # Individual blog post
│ ├── products/
│ │ ├── page.tsx # Product listing
│ │ └── [id]/
│ │ └── page.tsx # Product detail
│ └── dashboard/
│ └── page.tsx # Dashboard (noindex)
├── lib/
│ └── seo.ts # Shared SEO config
├── next.config.js
└── package.json
3Shared SEO config
import { createSEOConfig } from "react-ssr-seo-toolkit";
export const siteConfig = createSEOConfig({
titleTemplate: "%s | My Blog",
description: "A blog about web development and React.",
openGraph: {
siteName: "My Blog",
type: "website",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
site: "@myblog",
},
});
export const SITE_URL = "https://myblog.com";4Root layout
In Next.js App Router, the root layout provides the HTML shell. You do not need <SEOHead> here because Next.js injects metadata from generateMetadata automatically.
// Next.js root layout — handles <html> and <body>
// Metadata is injected automatically by Next.js
// from each page's generateMetadata() function
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<nav>{/* Shared navigation */}</nav>
<main>{children}</main>
<footer>{/* Shared footer */}</footer>
</body>
</html>
);
}Notice there is no <head> tag in the layout. Next.js manages the <head> automatically based on each page's generateMetadata export.
5Static page with metadata
For static pages, use mergeSEOConfig to build your metadata, then map the values to Next.js's Metadata format.
import {
mergeSEOConfig,
buildTitle,
buildCanonicalUrl,
} from "react-ssr-seo-toolkit";
import { siteConfig, SITE_URL } from "@/lib/seo";
import type { Metadata } from "next";
// Build SEO config using the toolkit
const seo = mergeSEOConfig(siteConfig, {
title: "About Us",
description: "Learn about our team and mission.",
canonical: buildCanonicalUrl(SITE_URL, "/about"),
});
// Export Next.js metadata
export const metadata: Metadata = {
title: buildTitle(seo.title!, seo.titleTemplate),
description: seo.description,
alternates: { canonical: seo.canonical },
openGraph: {
title: seo.title,
description: seo.description,
url: seo.canonical,
siteName: seo.openGraph?.siteName,
type: "website",
locale: seo.openGraph?.locale,
},
twitter: {
card: "summary_large_image",
site: seo.twitter?.site,
},
};
// Page component — just content, no <html> or <head>
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Our team and mission...</p>
</div>
);
}6Dynamic page with generateMetadata
For dynamic routes, use generateMetadata as an async function to fetch data and build SEO config. Add JSON-LD in the page component body.
import {
mergeSEOConfig,
buildTitle,
buildCanonicalUrl,
createArticleSchema,
createBreadcrumbSchema,
safeJsonLdSerialize,
} from "react-ssr-seo-toolkit";
import { siteConfig, SITE_URL } from "@/lib/seo";
import type { Metadata } from "next";
async function getPost(slug: string) {
// Replace with your data fetching logic
return {
title: "My Blog Post",
excerpt: "A short summary of the post.",
content: "Full post content...",
author: "Jane Doe",
date: "2025-06-15",
image: `${SITE_URL}/images/${slug}.jpg`,
slug,
};
}
// Dynamic metadata — runs on the server
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
const url = buildCanonicalUrl(SITE_URL, `/blog/${slug}`);
const seo = mergeSEOConfig(siteConfig, {
title: post.title,
description: post.excerpt,
canonical: url,
});
return {
title: buildTitle(seo.title!, seo.titleTemplate),
description: seo.description,
alternates: { canonical: url },
openGraph: {
title: post.title,
description: post.excerpt,
url,
type: "article",
publishedTime: post.date,
authors: [post.author],
images: [{ url: post.image }],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
},
};
}
// Page component — content + JSON-LD
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
const url = buildCanonicalUrl(SITE_URL, `/blog/${slug}`);
const articleSchema = createArticleSchema({
headline: post.title,
url,
datePublished: post.date,
author: [{ name: post.author }],
publisher: { name: "My Blog", logo: `${SITE_URL}/logo.png` },
images: [post.image],
});
const breadcrumb = createBreadcrumbSchema([
{ name: "Home", url: SITE_URL },
{ name: "Blog", url: buildCanonicalUrl(SITE_URL, "/blog") },
{ name: post.title, url },
]);
return (
<article>
{/* JSON-LD rendered as script tags in the page body */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: safeJsonLdSerialize(articleSchema),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: safeJsonLdSerialize(breadcrumb),
}}
/>
<h1>{post.title}</h1>
<p>By {post.author} on {post.date}</p>
<div>{post.content}</div>
</article>
);
}In Next.js App Router, JSON-LD <script> tags can go anywhere in the page body — they don't need to be in <head>. Google reads them from anywhere in the document.
7Dashboard page (noindex)
For pages that should not appear in search results (dashboards, admin panels), use the robots metadata field.
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Dashboard",
robots: {
index: false, // Tells search engines not to index this page
follow: true,
},
};
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<p>Your private dashboard content.</p>
</div>
);
}8Reusable metadata helper
To reduce repetition across pages, create a helper that converts the toolkit's SEOConfig into Next.js Metadata.
import {
mergeSEOConfig,
buildTitle,
buildCanonicalUrl,
} from "react-ssr-seo-toolkit";
import { siteConfig, SITE_URL } from "./seo";
import type { Metadata } from "next";
import type { SEOConfig } from "react-ssr-seo-toolkit";
/**
* Converts toolkit SEOConfig into Next.js Metadata.
* Use this in generateMetadata() or as a static export.
*/
export function buildMetadata(
pageConfig: Partial<SEOConfig>,
path?: string
): Metadata {
const seo = mergeSEOConfig(siteConfig, {
...pageConfig,
canonical: path
? buildCanonicalUrl(SITE_URL, path)
: pageConfig.canonical,
});
return {
title: buildTitle(seo.title ?? "", seo.titleTemplate),
description: seo.description,
alternates: seo.canonical
? { canonical: seo.canonical }
: undefined,
openGraph: {
title: seo.openGraph?.title ?? seo.title,
description: seo.openGraph?.description ?? seo.description,
url: seo.canonical,
siteName: seo.openGraph?.siteName,
type: (seo.openGraph?.type as "website") ?? "website",
locale: seo.openGraph?.locale,
},
twitter: {
card: (seo.twitter?.card as "summary_large_image") ?? "summary_large_image",
site: seo.twitter?.site,
creator: seo.twitter?.creator,
},
};
}Now any page can generate metadata in one line:
import { buildMetadata } from "@/lib/metadata";
// One line generates all metadata!
export const metadata = buildMetadata({
title: "About Us",
description: "Learn about our team.",
}, "/about");
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Our team and mission...</p>
</div>
);
}Next.js Pages Router
If you are using the Pages Router, you can use <SEOHead> via Next.js's <Head> component.
import Head from "next/head";
import {
mergeSEOConfig,
buildCanonicalUrl,
SEOHead,
} from "react-ssr-seo-toolkit";
import { siteConfig, SITE_URL } from "../lib/seo";
export default function AboutPage() {
const seo = mergeSEOConfig(siteConfig, {
title: "About Us",
description: "Learn about our team.",
canonical: buildCanonicalUrl(SITE_URL, "/about"),
});
return (
<>
<Head>
<SEOHead {...seo} />
</Head>
<div>
<h1>About Us</h1>
<p>Our team and mission...</p>
</div>
</>
);
}Summary
| Feature | App Router | Pages Router |
|---|---|---|
| Meta tags | generateMetadata() + builder functions | <Head><SEOHead /></Head> |
| JSON-LD | <script> in page body + safeJsonLdSerialize | <Head><JsonLd /></Head> or in body |
| Document | app/layout.tsx (no <SEOHead>) | _app.tsx / _document.tsx |
| Dynamic data | Async generateMetadata | getServerSideProps / getStaticProps |