Suggest an editImprove this articleRefine the answer for “Metadata and SEO in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Next.js metadata** is a built-in API for generating `<head>` tags for SEO. Export a `metadata` object for static pages or use `generateMetadata` for routes that depend on fetched data. ```tsx // Static export const metadata: Metadata = { title: 'IT Lead', description: 'Interview prep' } // Dynamic export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> { const doc = await getDoc(params.slug) return { title: doc.title, description: doc.description } } ``` **Key:** child page metadata merges with and overrides the parent layout's metadata automatically.Shown above the full answer for quick recall.Answer (EN)Image**Next.js metadata** is a declarative API that generates HTML `<head>` tags (title, description, OpenGraph) either statically from a plain export or dynamically via an async function that runs per request. ## Theory ### TL;DR - Static metadata exports a plain object at build time; `generateMetadata` runs per request and can fetch live data - Analogy: static is the default sign in the shop window; dynamic swaps it per product based on live inventory - `metadata` export in `layout.tsx` sets site-wide defaults; child pages override specific fields - Use static for fixed pages, dynamic for slug-based routes (blog, docs, products) - OG image URLs must be absolute or social crawlers ignore them ### Quick example ```tsx // app/layout.tsx - Static: applies to every page import type { Metadata } from 'next' export const metadata: Metadata = { title: { template: '%s | IT Lead', default: 'IT Lead' }, description: 'Interview prep platform for developers' } // Renders: <title>IT Lead</title> on root, "Page | IT Lead" on children // app/blog/[slug]/page.tsx - Dynamic: runs per request export async function generateMetadata( { params }: { params: { slug: string } } ): Promise<Metadata> { const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json()) return { title: post.title, // overrides the layout template description: post.excerpt } } ``` Root layout sets the baseline. Each route segment can override or extend it. Next.js merges them automatically. ### Static vs dynamic metadata Static metadata is a plain object exported from `layout.tsx` or `page.tsx`. Next.js reads it at build time (or once per SSR render) and injects the tags into `<head>`. No database calls, no waiting. Dynamic metadata lives in `generateMetadata`, an async function that receives `params` and `searchParams`. It runs on every request, so you can pull a blog post title from a CMS or a product name from a database. Next.js deduplicates fetch calls: if `generateMetadata` and the page component call the same `fetch`, the request runs once. Choose static for anything that does not change between users or requests. Choose dynamic when the `<head>` content depends on route params or live data. ### When to use - Fixed site title and description in root layout: static `metadata` export - Blog post or doc slug pulled from a database: `generateMetadata` in `[slug]/page.tsx` - E-commerce product pages with OG images from a CDN: dynamic with `openGraph.images` - Admin or internal pages with no public SEO value: set `robots: { index: false }` - Multilingual sites: dynamic with `alternates.languages` ### How Next.js handles metadata internally The App Router scans each route segment for a `metadata` export or `generateMetadata` function during the server render. It collects them from the root layout down to the current page, merges the objects (child fields override parent fields with the same key), and injects the result into `<head>` via React Server Components. For dynamic metadata, Next.js awaits the promise before streaming the response. Fetch calls inside `generateMetadata` use the same data cache as the page component, so duplicated fetches collapse into one. You control caching with `fetch({ next: { revalidate: 3600 } })` or tag-based invalidation via `revalidateTag`. ### OpenGraph and Twitter Cards Social crawlers (Facebook, LinkedIn, X) read OG and Twitter meta tags to build link previews. One consistent gotcha: OG image URLs must be absolute. A relative path like `/og.png` breaks social crawlers because they fetch the URL without your domain context. ```tsx export const metadata: Metadata = { openGraph: { title: 'JavaScript Interview Problems', description: 'Solve problems from real interviews', url: 'https://itlead.org/problems', siteName: 'IT Lead', images: [ { url: `${process.env.NEXT_PUBLIC_SITE_URL}/images/ITLeadBanner.png`, // absolute URL width: 1200, height: 630 } ], type: 'website' }, twitter: { card: 'summary_large_image', images: [`${process.env.NEXT_PUBLIC_SITE_URL}/images/ITLeadBanner.png`] } } ``` If `twitter` is partially omitted, Next.js inherits the missing values from `openGraph` automatically. ### Title templates A title template in the root layout adds a site name suffix to every child page without repeating it anywhere: ```tsx // app/layout.tsx export const metadata: Metadata = { title: { template: '%s | IT Lead', default: 'IT Lead' // shown when a child page sets no title } } // app/docs/page.tsx export const metadata: Metadata = { title: 'Knowledge Base' // Result: "Knowledge Base | IT Lead" } ``` `%s` is replaced with the child page's title string. `default` shows on pages that set no title of their own. ### Sitemap and robots.txt Next.js generates both as routes, not static files. Place them in `app/`: ```tsx // app/sitemap.ts import { MetadataRoute } from 'next' export default function sitemap(): MetadataRoute.Sitemap { const docs = getAllDocs() return [ { url: 'https://itlead.org', lastModified: new Date() }, ...docs.map(doc => ({ url: `https://itlead.org/docs/${doc.slug}`, lastModified: doc.updatedAt })) ] } // app/robots.ts export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/auth/'] }, sitemap: 'https://itlead.org/sitemap.xml' } } ``` Both are auto-served at `/sitemap.xml` and `/robots.txt`. ### Common mistakes **Mistake: exporting metadata from a Client Component** ```tsx 'use client' export const metadata = { title: 'Nope' } // silently ignored, no <head> injection ``` Metadata is processed only in Server Components. Move the export to the server `page.tsx` or `layout.tsx`. **Mistake: relative OG image URL** ```tsx openGraph: { images: '/og.png' } // breaks social crawlers ``` I've seen this exact issue in three separate production Next.js apps. Everything looks fine on localhost, then LinkedIn shows a broken preview. Fix: `${process.env.NEXT_PUBLIC_SITE_URL}/og.png`. **Mistake: missing `Promise<Metadata>` return type** ```tsx export async function generateMetadata({ params }) { // wrong inferred type return { title: 'Bad' } } ``` Add the explicit return type: `Promise<Metadata>`. Without it TypeScript cannot catch missing or incorrect fields at compile time. **Mistake: slow API call inside `generateMetadata` without caching** A blocking fetch in `generateMetadata` delays the entire server response. Use `fetch({ next: { revalidate: 3600 } })` for ISR or `cache: 'force-cache'` when the data rarely changes. **Mistake: no canonical URL in dynamic routes** Without `alternates.canonical`, search engines may treat `/blog/post?ref=twitter` and `/blog/post` as separate pages and split SEO signals between them. ### Real-world usage - nextjs.org docs: static base in root layout + dynamic per doc slug via Contentlayer - Supabase dashboard: `generateMetadata` pulls project names from the database per request - Linear.app: issue pages with `alternates.canonical` to consolidate SEO signals - E-commerce product pages: dynamic OG images from CDN with ISR revalidation every 5 minutes ### Follow-up questions **Q:** What is the difference between static metadata and `generateMetadata` in terms of render timing? **A:** Static metadata is read at build time (or once per SSR render) and costs nothing at request time. `generateMetadata` runs per request and can fetch fresh data, but adds latency without caching. **Q:** How does metadata inheritance work between layouts and pages? **A:** Next.js merges metadata from the root layout down to the current segment. Child fields override parent fields with the same key. A child page that sets `title` replaces the layout's title but inherits everything else unchanged. **Q:** Why do OG images need absolute URLs? **A:** Social crawlers (Facebook, Googlebot, X) do not know your domain. They fetch the URL as-is, so a relative path resolves to nothing outside your server context. **Q:** How do you add structured data (JSON-LD) for rich snippets in search results? **A:** Inject a `<script type="application/ld+json">` tag directly in the page component with `dangerouslySetInnerHTML`. The metadata API does not handle JSON-LD directly. **Q:** In a multi-tenant app, how would you generate tenant-specific metadata without leaking data across requests? **A:** Read the tenant identifier via `headers().get('x-tenant')` or `cookies()` inside `generateMetadata`. Call `unstable_noStore()` to opt out of caching, then use `fetch({ next: { tags: ['tenant-data'] } })` for on-demand revalidation when tenant data changes. Never store tenant state in module scope since serverless functions share no state between requests. ## Examples ### Static metadata in root layout ```tsx // app/layout.tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: { template: '%s | IT Lead', default: 'IT Lead' }, description: 'Interview prep platform for developers', keywords: ['frontend', 'interview', 'javascript', 'react'], openGraph: { siteName: 'IT Lead', type: 'website', images: [ { url: 'https://itlead.org/images/ITLeadBanner.png', // must be absolute width: 1200, height: 630 } ] } } ``` This runs once. Every page in the app inherits these defaults unless it sets its own values. ### Dynamic metadata for a blog post with OpenGraph ```tsx // app/blog/[slug]/page.tsx import type { Metadata } from 'next' import { getPost } from '@/lib/posts' export async function generateMetadata( { params }: { params: { slug: string } } ): Promise<Metadata> { const post = await getPost(params.slug) // cached by Next.js, shared with page component return { title: post.title, // becomes "How closures work | IT Lead" via template description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, type: 'article', publishedTime: post.date, images: post.image ? [{ url: post.image }] : [] }, twitter: { card: 'summary_large_image' } } } export default async function BlogPost({ params }) { const post = await getPost(params.slug) // same fetch - runs once total return <article>{post.content}</article> } ``` `getPost` appears twice in the file but runs once per request. Next.js deduplicates `fetch` calls within the same render cycle, so there is no double network hit. ### Structured data with JSON-LD for rich snippets ```tsx // app/blog/[slug]/page.tsx export default async function BlogPost({ params }) { const post = await getPost(params.slug) const jsonLd = { '@context': 'https://schema.org', '@type': 'Article', headline: post.title, description: post.excerpt, datePublished: post.date, author: { '@type': 'Organization', name: 'IT Lead' } } return ( <> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> <article>{post.content}</article> </> ) } ``` JSON-LD lives in the page component, not in the metadata API. Google reads it for article cards, breadcrumbs, and FAQ boxes in search results.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.