How to Use the Next.js Metadata API for SEO

Modern React apps live or die by discoverability. If search engines and AI crawlers cannot parse titles, canonicals, or structured data, your pages will underperform regardless of content quality.
This guide explains how to use the Next.js metadata API to ship reliable, production-grade SEO in SSR and SSG apps. It is for developers and SaaS teams building with the App Router who need consistent metadata, schema, and sitemaps without a CMS. The key takeaway: model metadata as code using the metadata API, validate it at build time, and automate it across your blog and docs.
What is the Next.js metadata API?
The Next.js metadata API is a first-class way to declare page metadata in App Router projects. Instead of hand-writing tags in head elements, you export objects or a generateMetadata function that Next.js serializes into SEO-friendly tags during render.
Why it exists
- Centralize SEO configuration in code
- Avoid brittle, ad hoc head tags
- Enable consistent defaults with easy overrides per route
How it works at a high level
- In a layout or page, export a metadata object or a generateMetadata function
- Next.js merges metadata from root to leaf layouts and the active page
- The framework renders tags server side for search engines
For official docs and option references, see the Next.js Metadata docs: https://nextjs.org/docs/app/api-reference/functions/generate-metadata.
Core concepts and types in the metadata API
Understanding the main fields will help you enforce an SEO baseline across your app.
Top level fields you will use often
- title: Static string or template with default and route-specific titles
- description: One sentence that matches on-page content
- keywords: Short list of relevant terms
- openGraph: Title, description, URL, siteName, images, type, and locale
- twitter: Card type, title, description, images
- alternates: Canonicals and language alternates
- robots: Indexing and crawling rules
- icons: Favicons and touch icons
Where metadata is declared
- app/layout.tsx or app/layout.ts for global defaults
- route groups and nested layouts for section-specific defaults
- app/[slug]/page.tsx for per-page overrides via generateMetadata
Setting up global defaults with metadata
Clean defaults save time and prevent SEO drift.
Example: app/layout.tsx
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
title: {
default: 'Example SaaS',
template: '%s | Example SaaS'
},
description: 'Programmatic SEO and a reliable React SEO pipeline for your product.',
keywords: ['Next.js SEO', 'programmatic SEO', 'React SEO'],
openGraph: {
siteName: 'Example SaaS',
type: 'website',
url: 'https://example.com',
images: ['/og-default.png']
},
twitter: {
card: 'summary_large_image',
creator: '@examplesaas'
},
robots: {
index: true,
follow: true
},
alternates: {
canonical: '/',
}
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Tips for reliable defaults
- Set metadataBase to ensure absolute URLs in openGraph and alternates
- Use title templates to keep titles consistent
- Provide a fallback OG image so every route shares a valid card
Route level metadata with generateMetadata
For dynamic content like blog posts or docs, use generateMetadata to compute tags from data.
Example: app/blog/[slug]/page.tsx
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
import { getPostBySlug } from '@/lib/data';
export async function generateMetadata(
{ params }: { params: { slug: string } },
parent: ResolvingMetadata
): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
if (!post) return { title: 'Not found', robots: { index: false, follow: false } };
const url = new URL(`/blog/${post.slug}`, 'https://example.com');
return {
title: post.seoTitle ?? post.title,
description: post.seoDescription ?? post.excerpt,
keywords: post.keywords,
alternates: { canonical: url.toString() },
openGraph: {
type: 'article',
url: url.toString(),
title: post.title,
description: post.excerpt,
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
images: [{ url: post.ogImage ?? '/og-default.png' }]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.ogImage ?? '/og-default.png']
}
};
}
export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}
Good practices for dynamic routes
- Use content data as the single source of truth
- Always compute canonical URLs for dynamic slugs
- Mark 404 like states as noindex to avoid thin content indexing
Canonicals, language alternates, and robots
The alternates and robots fields help control indexing and duplicate risk.
Canonical URLs
- Use alternates.canonical at global and page levels
- For cross-posted content, set the canonical to the primary source
Language alternates
- Provide alternates.languages for locales, like en, fr, es
- Ensure localized pages have self-referencing canonicals and hreflang
alternates: {
canonical: 'https://example.com/blog/post',
languages: {
'en-US': 'https://example.com/en/blog/post',
'fr-FR': 'https://example.com/fr/blog/post'
}
}
Robots
- For private previews or staging, set robots to noindex and add an x-robots-tag header at the edge
- For paginated lists, decide on indexation strategy and include link rel prev and next in markup if relevant
Open Graph and Twitter cards that always render
Social cards drive click-through and help AI crawlers form page summaries.
Recommended OG structure
- Always include a 1200x630 image
- Use absolute URLs if you do not set metadataBase
- Match title and description to the HTML content
openGraph: {
type: 'article',
url: 'https://example.com/blog/nextjs-metadata-api',
title: 'How to Use the Next.js Metadata API for SEO',
description: 'Practical patterns for SSR apps.',
images: [{ url: 'https://example.com/og/nextjs-metadata.png', width: 1200, height: 630 }]
}
Twitter card basics
- Use summary_large_image for most articles
- Keep descriptions under ~200 characters
Structured data with JSON-LD in App Router
The metadata API focuses on head tags, but you also need structured data for articles, products, and org profiles. In the App Router, emit JSON-LD within your page or layout.
Simple Article schema helper
// app/components/JsonLd.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
// app/blog/[slug]/page.tsx
import { JsonLd } from '@/app/components/JsonLd';
export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
const schema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: [{ '@type': 'Person', name: post.author }],
image: post.ogImage
};
return (
<article>
<JsonLd data={schema} />
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
}
Validation tools
- Test rich results with Google Rich Results Test: https://search.google.com/test/rich-results
- Validate structured data syntax with Schema.org tools: https://validator.schema.org/
Sitemaps and robots.txt in Next.js
Next.js provides first-class functions to generate sitemaps and robots.txt alongside metadata.
Dynamic sitemap generation
// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { listPublishedPosts } from '@/lib/data';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await listPublishedPosts();
const base = 'https://example.com';
return [
{ url: `${base}/`, changefreq: 'weekly', priority: 1 },
...posts.map((p) => ({
url: `${base}/blog/${p.slug}`,
lastModified: p.updatedAt,
changefreq: 'monthly',
priority: 0.7
}))
];
}
robots.txt with crawl rules
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const base = 'https://example.com';
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/drafts', '/api']
},
sitemap: [`${base}/sitemap.xml`]
};
}
For more on these routes, see the Next.js Metadata and SEO routes docs: https://nextjs.org/docs/app/building-your-application/optimizing/metadata.
Production checklist for the Next.js metadata API
Use this quick list to prevent missed tags and indexing issues at launch.
Global and layout level
- metadataBase set to your canonical origin
- Default title template and OG image
- robots index and follow set as intended
Page level
- generateMetadata uses real content fields
- Canonical URLs computed per slug
- openGraph and twitter images present
Structured data
- Article or Product JSON-LD emitted where relevant
- Validation passing in Rich Results Test
Crawling and sitemaps
- robots.ts lists the correct sitemap
- app/sitemap.ts includes canonical URLs and lastModified
Automating blog SEO with a React SEO pipeline
Once your app-level patterns are stable, wire a pipeline that enforces them for every new post. This is where programmatic SEO and AI blog automation can help your team scale without regressions.
From content to publish
- Store post data with SEO fields: title, excerpt, slug, publishedAt, ogImage
- Generate HTML and images in CI or via a content service
- Publish content and let your Next.js app render with generateMetadata
Benefits of treating SEO as code
- Consistency across hundreds of posts
- Easy refactors when requirements change
- Simple testing of metadata outputs per route
For a developer-first tool that automates metadata, schema, sitemaps, and internal linking while generating production-ready posts, see AutoBlogWriter: https://autoblogwriter.app/.
Example: end to end blog route with enforced SEO
Below is a compact example that pulls together data, metadata, JSON-LD, and a render path.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getPostBySlug } from '@/lib/data';
import { JsonLd } from '@/app/components/JsonLd';
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
if (!post) return { title: 'Not found', robots: { index: false, follow: false } };
const url = `https://example.com/blog/${post.slug}`;
return {
title: post.seoTitle ?? post.title,
description: post.seoDescription ?? post.excerpt,
alternates: { canonical: url },
openGraph: { type: 'article', url, title: post.title, description: post.excerpt, images: [{ url: post.ogImage ?? '/og-default.png' }] },
twitter: { card: 'summary_large_image', title: post.title, description: post.excerpt, images: [post.ogImage ?? '/og-default.png'] }
};
}
export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) return notFound();
const schema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: [{ '@type': 'Person', name: post.author }],
image: post.ogImage
};
return (
<article>
<JsonLd data={schema} />
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
}
Common pitfalls and how to avoid them
These are the issues that most often block indexing or break social cards.
Relative URLs in OG images
- Use metadataBase or absolute URLs for images and canonical links
Missing canonicals on dynamic routes
- Always compute alternates.canonical based on the slug
Inconsistent titles across cards and HTML
- Align title and description in metadata with your rendered H1 and intro
No JSON-LD when rich results are expected
- Emit JSON-LD for articles, products, FAQs, and breadcrumbs where applicable
- Validate after deployment, not just locally
Tooling and resources
A small set of links to keep handy when working with the Next.js metadata API and broader React SEO pipeline.
Docs and references
- Next.js Metadata API: https://nextjs.org/docs/app/api-reference/functions/generate-metadata
- Next.js SEO routes overview: https://nextjs.org/docs/app/building-your-application/optimizing/metadata
- Google Search Central basics: https://developers.google.com/search/docs/fundamentals/seo-starter-guide
Validators and debuggers
- Rich Results Test: https://search.google.com/test/rich-results
- URL Inspection in Search Console: https://search.google.com/search-console
- Open Graph debugger: https://developers.facebook.com/tools/debug/
- Twitter Card validator: https://cards-dev.twitter.com/validator
Automation and pipelines
- AutoBlogWriter for programmatic SEO and automated blogging workflow: https://autoblogwriter.app/
Comparison: metadata API vs manual head tags
Here is a quick comparison to help teams decide whether to migrate.
The table below contrasts the metadata API with manual head tags across core concerns.
| Concern | Next.js metadata API | Manual head tags |
|---|---|---|
| Consistency | Enforced via types and defaults | Prone to drift across pages |
| DX | Central, typed, composable | Scattered and brittle |
| Dynamic data | First class with generateMetadata | Requires custom logic |
| Social cards | Structured objects with images | Manual link and meta tags |
| Sitemaps/robots | First class routes | Custom scripts needed |
| Validation | Easier to test per route | Ad hoc and error prone |
Key Takeaways
- Use the Next.js metadata API as the single source of truth for titles, canonicals, and cards
- Compute per-route metadata with generateMetadata for dynamic content
- Emit JSON-LD where rich results matter and validate after deploy
- Generate sitemaps and robots via first class Next.js routes
- Treat SEO as code and automate with a React SEO pipeline for scale
Ship metadata once, enforce it everywhere, and let your app publish with confidence.
Frequently Asked Questions
- What is the Next.js metadata API?
- It is a typed API in the App Router to declare SEO metadata as code. Next.js merges and renders it server side into tags for crawlers.
- Should I use metadata or manual head tags?
- Prefer the metadata API for consistency, typed safety, and dynamic generation. Manual tags are harder to maintain at scale.
- How do I set a canonical URL per page?
- Return alternates.canonical in generateMetadata, using your site origin and the current slug to build the absolute URL.
- Can I add JSON-LD with the metadata API?
- JSON-LD is added as a script tag in your component. Use a small helper to inject application/ld+json with your schema object.
- How do I generate a sitemap in Next.js?
- Create app/sitemap.ts and return an array of URL entries. You can pull slugs from your data source and include lastModified.