Master Next.js Metadata API for Better SEO
Learn how to use the Next.js Metadata API to boost SEO with dynamic titles, Open Graph tags, and structured data in the App Router.
Tags
Master Next.js Metadata API for Better SEO
This is part of the AI Automation Engineer Roadmap series.
TL;DR
Use the generateMetadata function in Next.js App Router to create dynamic, per-page SEO metadata with full type safety.
Why This Matters
Dynamic pages like blog posts and product pages each need unique titles, descriptions, and Open Graph images. Hardcoding a static metadata export doesn't work when the content comes from a database or CMS. You need a way to fetch data and generate metadata dynamically for each route.
The Metadata API matters because it centralizes core technical SEO concerns in a framework-native way:
- ›page titles and descriptions
- ›canonical URLs
- ›Open Graph and Twitter cards
- ›robots directives
- ›alternate language references
- ›route-specific metadata for dynamic pages
If you misuse it, your pages can end up with duplicated titles, broken social previews, or stale metadata on key content routes. If you use it correctly, you get a cleaner implementation and better consistency across the app.
Static vs Dynamic Metadata
Next.js gives you two main patterns:
- ›
export const metadatafor static route metadata - ›
export async function generateMetadata()for dynamic metadata
Use the static export when the route content is fixed, such as an about page or contact page. Use generateMetadata when metadata depends on fetched data, route params, or content files.
Static Example
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Contact",
description: "Get in touch for web development and AI automation work.",
};
export default function ContactPage() {
return <main>...</main>;
}Dynamic Example
Export an async generateMetadata function from your page file:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>
r.json()
);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage, width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
},
alternates: {
canonical: `https://yoursite.com/blog/${slug}`,
},
};
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
// render post...
}Next.js deduplicates the fetch call, so generateMetadata and your page component share the same request without any extra work.
What Metadata You Should Usually Set
For most content pages, the useful baseline is:
- ›
title - ›
description - ›
alternates.canonical - ›
openGraph.title - ›
openGraph.description - ›
openGraph.url - ›
openGraph.images - ›
twitter.card - ›
robots
If the page is indexable and shareable, those fields do most of the work.
return {
title: post.title,
description: post.excerpt,
alternates: {
canonical: `https://yoursite.com/blog/${slug}`,
},
openGraph: {
type: "article",
url: `https://yoursite.com/blog/${slug}`,
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage, width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
robots: {
index: true,
follow: true,
},
};Common Patterns That Work Well
1. Share SEO Data Between the Page and Metadata
If you fetch the same content in the page and in generateMetadata, keep the data-loading logic in one reusable function.
async function getPost(slug: string) {
return fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 },
}).then((r) => r.json());
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}This keeps metadata and rendered content aligned.
2. Return a Fallback When Content Is Missing
Do not assume the content always exists. If a slug is invalid or a CMS item is unpublished, your metadata should still behave predictably.
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return {
title: "Post Not Found",
description: "The requested article could not be found.",
robots: { index: false, follow: false },
};
}
return {
title: post.title,
description: post.excerpt,
};
}3. Use Canonicals Intentionally
Canonical URLs are especially important when:
- ›query params create multiple URL variants
- ›filtered pages are crawlable
- ›content is syndicated
- ›you have both category and tag routes
The Metadata API makes it easy to enforce one primary URL per page.
Where the Metadata API Ends
The Metadata API handles head tags. It does not replace everything related to SEO.
You still need:
- ›good on-page copy
- ›structured data where appropriate
- ›solid internal linking
- ›proper robots and sitemap handling
- ›clean heading hierarchy
A lot of teams expect metadata alone to fix discoverability. It will not. It is necessary, but not sufficient.
Common Mistakes
Treating Metadata as a One-Time Setup
Metadata should reflect the route. A single global title template is useful, but dynamic pages still need unique per-item values.
Forgetting Social Images
Pages without route-specific Open Graph images often look generic when shared. If the page matters for discovery or distribution, the preview asset matters too.
Mismatching Page Content and Metadata
If the title tag says one thing and the on-page heading says another, search engines and users get mixed signals.
Building Canonicals Incorrectly
A canonical URL should be the final preferred URL. Accidentally using a relative path, a staging domain, or the wrong slug is a common mistake.
Production Recommendations
If you are shipping a content-heavy Next.js app, the clean pattern is:
- ›centralize site-level constants
- ›keep route-level metadata generation close to content loading
- ›use route-specific Open Graph images where possible
- ›set explicit canonical URLs
- ›combine Metadata API with JSON-LD for key pages
That gives you a strong technical foundation without scattering SEO logic across the app.
Why This Works
The generateMetadata function runs on the server at request time (or build time for static routes) and returns a typed Metadata object. Next.js takes this object and renders the correct <title>, <meta>, and <link> tags in the document head. Because it's a standard async function, you can fetch data, read from a database, or compute values -- all with full TypeScript autocomplete on the return type. The framework handles deduplication, streaming, and proper tag ordering automatically.
Final Takeaway
The Next.js Metadata API is the right default for App Router SEO. Use static metadata for fixed routes, generateMetadata for dynamic content, and pair it with canonical URLs, social previews, and structured data for the routes that actually matter.
Collaboration
Need help with a project?
Let's Build It
I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.
Related Articles
TypeScript Utility Types You Should Know
Five essential built-in generic utility types in TypeScript that will save you hundreds of lines of code.
Generate Dynamic OG Images in Next.js
Generate dynamic Open Graph images in Next.js using the ImageResponse API with custom fonts, gradients, and data-driven content for social sharing.
GitHub Actions Reusable Workflows: Stop Repeating Yourself
Create reusable GitHub Actions workflows with inputs, secrets, and outputs to eliminate YAML duplication across repositories and teams efficiently.