Loading…
Loading…
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
This is part of the AI Automation Engineer Roadmap series.
Use the generateMetadata function in Next.js App Router to create dynamic, per-page SEO metadata with full type safety.
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:
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.
Next.js gives you two main patterns:
export const metadata for static route metadataexport async function generateMetadata() for dynamic metadataUse 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.
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>;
}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.
For most content pages, the useful baseline is:
titledescriptionalternates.canonicalopenGraph.titleopenGraph.descriptionopenGraph.urlopenGraph.imagestwitter.cardrobotsIf 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,
},
};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.
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,
};
}Canonical URLs are especially important when:
The Metadata API makes it easy to enforce one primary URL per page.
The Metadata API handles head tags. It does not replace everything related to SEO.
You still need:
A lot of teams expect metadata alone to fix discoverability. It will not. It is necessary, but not sufficient.
Metadata should reflect the route. A single global title template is useful, but dynamic pages still need unique per-item values.
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.
If the title tag says one thing and the on-page heading says another, search engines and users get mixed signals.
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.
If you are shipping a content-heavy Next.js app, the clean pattern is:
That gives you a strong technical foundation without scattering SEO logic across the app.
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.
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
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.
Five essential built-in generic utility types in TypeScript that will save you hundreds of lines of code.
Generate dynamic Open Graph images in Next.js using the ImageResponse API with custom fonts, gradients, and data-driven content for social sharing.
Create reusable GitHub Actions workflows with inputs, secrets, and outputs to eliminate YAML duplication across repositories and teams efficiently.