Sanity

Dynamic Metadata in Next.js: Complete SEO Guide for Developers

Master Next.js dynamic metadata with this hands-on guide. Learn generateMetadata, Open Graph images, Twitter Cards, CMS-driven SEO, and best practices to boost your app's search visibility.

June 26, 202611 min readMuhammad Zohaib Ramzan
AI-generated editorial illustration created for this article.

Image detail

Editorial illustration of a set of distinct page mirrors each reflecting its own title and preview card

From reading to action

A practical path through this guide.

Select a step to explore what to do and why it matters.

STEP 01 / 03

Choose fixed or fetched metadata

Match the metadata approach to the route’s content dependencies.

Search engines rely on metadata to understand, index, and rank your pages — and in modern web apps, that metadata often needs to be dynamic. Next.js 13+ introduced a powerful, file-based metadata API that makes it straightforward to generate per-page SEO tags, Open Graph images, and Twitter Cards at scale. Whether you're building a blog, an e-commerce store, or a CMS-driven marketing site, mastering Next.js dynamic metadata is one of the highest-leverage SEO investments you can make.

Static vs Dynamic Metadata

Next.js supports two complementary approaches to metadata. Static metadata is defined by exporting a plain metadata object from a page.tsx or layout.tsx file. It is evaluated once at build time and is ideal for pages whose SEO content never changes — your homepage, about page, or pricing page, for example.

// app/about/page.tsx export const metadata = { title: 'About Us | Acme Corp', description: 'Learn about the team behind Acme Corp.' };

Dynamic metadata, on the other hand, is generated at request time (or at build time during static generation) by exporting an async generateMetadata function. Use it whenever the page title, description, or social images depend on data — a blog post slug, a product ID, a user profile, and so on.

The two approaches are mutually exclusive per route segment: you can export either metadata or generateMetadata, never both.

The generateMetadata Function

generateMetadata is an async function exported from a page.tsx or layout.tsx. Next.js calls it server-side and merges the returned object into the document <head>. The function receives two arguments: props (including params and searchParams) and parent — a ResolvingMetadata promise that resolves to the parent segment's metadata, enabling inheritance.

import type { Metadata } from 'next'; export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> { const post = await fetchPost(params.slug); return { title: post.title, description: post.excerpt }; }

Next.js deduplicates fetch calls automatically, so fetching the same resource in both generateMetadata and the page component costs only one network round-trip.

Metadata for Dynamic Routes

Dynamic routes — those with [slug], [id], or catch-all [...segments] segments — are the most common use case for generateMetadata. Here is a complete example for a blog post route at app/blog/[slug]/page.tsx:

export async function generateMetadata({ params }, parent) { const post = await getPost(params.slug); if (!post) return {}; const previousImages = (await parent).openGraph?.images ?? []; return { title: post.title, description: post.excerpt, openGraph: { images: [post.coverImageUrl, ...previousImages] } }; }

Key points: return an empty object {} rather than throwing when data is missing so Next.js falls back to parent metadata. Use next: { revalidate: N } on your fetch to control ISR freshness, and pair generateStaticParams with generateMetadata to pre-render dynamic routes at build time.

Inheriting and Merging Metadata

Next.js merges metadata from the root layout down through nested layouts to the page. Each segment can extend or override its parent. The parent argument gives you access to the resolved parent metadata before your function returns, which is useful for appending to arrays rather than replacing them.

const parentKeywords = (await parent).keywords ?? []; return { keywords: [...parentKeywords, 'nextjs', 'seo', 'metadata'] };

For the title field, Next.js provides a template shorthand in layouts. Set title: { template: '%s | My Site', default: 'My Site' } in your root layout, and any child page that sets title: 'About' will automatically render as About | My Site — no manual string concatenation needed.

Open Graph Images with ImageResponse

Open Graph images dramatically increase click-through rates on social media. Next.js ships a built-in ImageResponse API (powered by Satori) that lets you generate OG images on-demand using JSX. Create a file at app/blog/[slug]/opengraph-image.tsx and export a default function that returns a new ImageResponse(...) with your JSX layout.

export const runtime = 'edge'; export const size = { width: 1200, height: 630 }; export default async function OgImage({ params }) { const post = await fetchPost(params.slug); return new ImageResponse(<div style={{ background: '#0f172a', color: '#f8fafc' }}>{post.title}</div>, { ...size }); }

Next.js automatically wires this file to the og:image meta tag for the route — no manual openGraph.images configuration required. The Edge Runtime keeps cold starts minimal.

Twitter Card Metadata

Twitter reads Open Graph tags as a fallback, but you can provide Twitter-specific overrides using the twitter key in your metadata return value:

return { twitter: { card: 'summary_large_image', title: post.title, description: post.excerpt, images: [post.coverImageUrl], creator: '@yourhandle' } };

The card type controls the card layout: summary for a small square thumbnail, summary_large_image for a large banner (ideal for blog posts), app for mobile app promotion, and player for embedded video or audio. Always validate your cards with the Twitter Card Validator before shipping.

Metadata for CMS-Driven Pages

When content lives in a headless CMS (Sanity, Contentful, Strapi, etc.), generateMetadata becomes the bridge between your CMS data model and the browser <head>. A Sanity-powered example using GROQ:

const postMetaQuery = groq`*[_type == "post" && slug.current == $slug][0]{ title, excerpt, "ogImageUrl": mainImage.asset->url }`; export async function generateMetadata({ params }) { const post = await client.fetch(postMetaQuery, { slug: params.slug }); if (!post) return {}; return { title: post.title, description: post.excerpt, openGraph: { images: post.ogImageUrl ? [{ url: post.ogImageUrl, width: 1200, height: 630 }] : [] } }; }

Best practices for CMS-driven metadata: project only the fields you need in your metadata query; cache aggressively with next: { tags: ['post', params.slug] } and revalidate on CMS webhooks; provide fallbacks for every field in case editors leave SEO fields blank; and validate image dimensions — OG images should be 1200×630 px for best compatibility.

Common Mistakes

1. Exporting both metadata and generateMetadata. Next.js will throw a build error. Pick one per route segment.

2. Not awaiting the parent argument. The parent parameter is a Promise. Forgetting await parent before accessing its properties causes a runtime error.

3. Hardcoding absolute URLs. Always set metadataBase: new URL('https://www.example.com') in your root layout so relative URLs in openGraph.images resolve correctly.

4. Skipping generateStaticParams. For statically generated dynamic routes, omitting generateStaticParams means metadata is only generated at request time, defeating the purpose of static generation.

5. Duplicate title tags. If you set title in both a layout and a page without using the template system, you'll end up with conflicting titles. Use title.template in layouts and plain strings in pages.

6. Ignoring the robots field. Set robots: { index: false, follow: false } on pages you don't want indexed — admin routes, preview pages, and staging environments.

Best Practices

  • Set metadataBase in the root layout — it is required for all absolute URL resolution.
  • Use the title template system — keeps brand suffixes consistent without repetition.
  • Keep descriptions under 160 characters — longer descriptions are truncated in SERPs.
  • Always provide alt text for OG images — it improves accessibility and is required by some validators.
  • Co-locate opengraph-image.tsx with the route — Next.js auto-wires it; no manual config needed.
  • Revalidate on CMS publish events — use revalidateTag in a webhook API route to keep metadata fresh without full rebuilds.
  • Test with real tools — use Google's Rich Results Test, Facebook Sharing Debugger, and Twitter Card Validator before launch.
  • Use structured data alongside metadata — JSON-LD via <Script> complements <meta> tags for rich snippets.

Conclusion

Next.js dynamic metadata gives you a clean, composable, and type-safe API to control every SEO signal your pages emit — from <title> and <meta description> to Open Graph images and Twitter Cards. By combining generateMetadata with the file-based opengraph-image convention, the title template system, and disciplined CMS integration, you can build sites that are both developer-friendly and search-engine-optimized from day one. Start with metadataBase in your root layout, adopt the title template pattern, and layer in dynamic generation only where your content actually varies — your future self (and your search rankings) will thank you.

A few practical answers

Frequently asked questions

Should static and dynamic metadata both be exported from one segment?

Choose the approach appropriate to that segment. Use a metadata object for fixed values or generateMetadata when values depend on route content.

Why should inherited sharing metadata be inspected after an override?

Nested metadata can replace fields that were supplied by a parent. Check resolved titles, descriptions, images and canonical URLs on the actual public response.