November 3, 2026·5 min read·By NitroSERP Editorial Team
Updated
Next.js App Router Metadata API, OpenGraph & JSON-LD Guide (2026)
Copy-and-paste Next.js 15 & 16 App Router Metadata API TypeScript examples for title templates, canonical URLs, OpenGraph images, robots rules, and JSON-LD.
The Next.js App Router Metadata API replaces manual <head> tags with strongly-typed TypeScript exports (export const metadata: Metadata for static routes and export async function generateMetadata() for dynamic routes) in app/layout.tsx and app/page.tsx. Properly configured, it guarantees that every route emits a self-referencing canonical URL, OpenGraph and Twitter social cards, googleBot snippet directives, and clean JSON-LD structured data on initial server render.
To generate a complete, copy-pasteable TypeScript configuration for your project in seconds, use our free Next.js Metadata Generator.
Next.js App Router Metadata API Quick Reference Matrix
| Metadata Field | Where to Define It | What It Renders in HTML <head> | Common Mistake to Avoid |
|---|---|---|---|
metadataBase | Root app/layout.tsx | Resolves relative OpenGraph, Twitter, and canonical paths into absolute https:// URLs | Omitting metadataBase, which causes Next.js build warnings and broken relative og:image URLs |
title.template | Root app/layout.tsx | Automatically appends your brand suffix (e.g. %s | Brand) to child route titles | Hardcoding the brand suffix inside every child page's title string, resulting in duplicate suffixes (Page | Brand | Brand) |
alternates.canonical | Every indexable page.tsx | <link rel="canonical" href="https://..." /> | Setting alternates.canonical: "/" inside root app/layout.tsx without overriding it in child pages (which points every page's canonical to the homepage) |
openGraph | layout.tsx + override in page.tsx | og:title, og:description, og:url, og:image, og:type | Leaving openGraph out of child pages—Next.js does a shallow merge, so child pages should explicitly pass their own openGraph.title and openGraph.url |
robots.googleBot | Root app/layout.tsx (or noindex pages) | <meta name="robots"> and <meta name="googlebot"> directives | Forgetting max-image-preview: "large", which is required for full-width Google Discover and mobile SERP thumbnails |
1. Root Layout Boilerplate (app/layout.tsx) with metadataBase & Title Templates
Place your global defaults (metadataBase, title.template, and default robots directives) in app/layout.tsx—never put a static alternates.canonical in app/layout.tsx unless every child route overrides it:
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://www.example.com"),
title: {
default: "Acme Dev — Fast Developer & SEO Utilities",
template: "%s | Acme Dev",
},
description:
"Free browser-based developer and technical SEO utilities with instant TypeScript and JSON-LD output.",
openGraph: {
type: "website",
locale: "en_US",
siteName: "Acme Dev",
images: [
{
url: "/og-default.png",
width: 1200,
height: 630,
alt: "Acme Dev Tools Preview",
},
],
},
twitter: {
card: "summary_large_image",
creator: "@acmedev",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
2. Static Page Metadata Export (app/pricing/page.tsx)
For static pages, export a metadata: Metadata object with a self-referencing alternates.canonical path and route-specific openGraph fields (or build it visually in the Next.js Metadata Generator):
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Pricing & Plans",
description:
"Compare free and pro plans with transparent monthly pricing and zero hidden usage fees.",
alternates: {
canonical: "/pricing",
},
openGraph: {
title: "Pricing & Plans | Acme Dev",
description:
"Compare free and pro plans with transparent monthly pricing and zero hidden usage fees.",
url: "/pricing",
},
};
Before deploying, paste your rendered title and description into the Google SERP Preview Tool to confirm they stay within desktop pixel and character limits.
3. Dynamic Route generateMetadata() (app/blog/[slug]/page.tsx)
In Next.js 15 and 16, route params is a Promise that must be awaited inside generateMetadata():
import type { Metadata } from "next";
import { notFound } from "next/navigation";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return {};
return {
title: post.title,
description: post.excerpt,
alternates: {
canonical: `/blog/${slug}`,
},
openGraph: {
type: "article",
title: post.title,
description: post.excerpt,
url: `/blog/${slug}`,
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
images: [
{
url: post.ogImage || `/blog/${slug}/opengraph-image`,
width: 1200,
height: 630,
alt: post.title,
},
],
},
};
}
4. Adding XSS-Safe JSON-LD Structured Data in Next.js App Router
Next.js recommends rendering JSON-LD via a standard <script type="application/ld+json"> inside your Server Component while escaping < as \u003c so user content can never break out of the script tag. You can generate validated Article, BreadcrumbList, and FAQPage schemas in our Schema Markup Generator:
export default function BlogPostPage() {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: "Next.js App Router Metadata API Guide (2026)",
datePublished: "2026-11-03",
dateModified: "2026-11-03",
author: {
"@type": "Organization",
name: "NitroSERP Editorial Team",
},
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
{/* Page content */}
</article>
);
}
5. Complete Next.js Technical SEO Checklist (robots.ts, sitemap.ts & /llms.txt)
- Generate
export const metadataorgenerateMetadata(): Use the Next.js Metadata Generator so every indexable page has a unique title, description, and self-referencing canonical URL. - Configure AI & Search Crawler Rules (
robots.txt): AllowGooglebotandOAI-SearchBotwhile blocking training scrapers (GPTBot,CCBot) using the Robots.txt Generator (read our OAI-SearchBot vs. GPTBot guide). - Publish
/public/llms.txt: Create a clean Markdown map of your key routes for ChatGPT, Claude, and Perplexity with the freellms.txtGenerator. - Verify
app/sitemap.tsin Search Console: Ensure your XML sitemap returnsContent-Type: application/xmland zero301/404URLs—see our guide to fix "Sitemap could not be read" in Google Search Console.
Related SEO Guides
- Fix 'Sitemap Could Not Be Read' & 'Couldn't Fetch' in Google Search Console (2026)Step-by-step fix for Google Search Console's 'Couldn't fetch' and 'Sitemap could not be read' XML sitemap errors. Check HTTP status codes, XML namespaces, robots.txt rules, and Next.js / WordPress fixes.
- OAI-SearchBot vs GPTBot: Allow ChatGPT Search & Block AI Training in robots.txtLearn the exact difference between OpenAI's OAI-SearchBot, GPTBot, and ChatGPT-User crawlers. Copy a tested robots.txt snippet that allows ChatGPT Search citations while blocking AI model training.