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
Next.js dynamic metadata configuration for SEO — code editor showing meta tags and Open Graph settings

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.

FAQ

Q: Can I use generateMetadata in a Server Component layout?

Yes. generateMetadata is always a server-side function. Client Components cannot export generateMetadata — it must live in a Server Component file.

Q: Does generateMetadata run on every request?

It depends on your rendering strategy. For dynamically rendered routes it runs on every request. For statically generated routes (via generateStaticParams) it runs once at build time per param combination. With ISR it runs when the cache is revalidated.

Q: How do I add JSON-LD structured data?

Next.js doesn't have a dedicated API for JSON-LD. The recommended approach is to render a <script type="application/ld+json"> tag directly inside your page or layout component using dangerouslySetInnerHTML.

Q: What's the difference between openGraph.images and the opengraph-image file convention?

The file convention (opengraph-image.tsx or opengraph-image.png) is the recommended approach — Next.js handles URL generation, caching headers, and <meta> tag injection automatically. The openGraph.images array is a manual override, useful when your image URL comes from an external CDN or CMS.

Q: Can I set metadata conditionally based on environment?

Absolutely. Since generateMetadata is just an async function, you can branch on process.env.NODE_ENV or any environment variable. This is a clean way to prevent staging or preview environments from being indexed by setting robots: { index: false, follow: false } in non-production environments.

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.