SyntaxStudy
Sign Up
Next.js Static and Dynamic Metadata with the Metadata API
Next.js Beginner 1 min read

Static and Dynamic Metadata with the Metadata API

Next.js 13.2 introduced a built-in Metadata API that replaces the need for the react-helmet or next/head libraries. You define metadata by exporting a metadata object or a generateMetadata function from layout.tsx or page.tsx files. Next.js merges metadata from the root layout down to the current page, with deeper segments overriding values from parent segments. The metadata object supports a comprehensive set of fields covering HTML meta tags, Open Graph tags for social sharing, Twitter Card tags, and more. The title field can be a plain string or a template object with a template property and a default. Using a title template like '%s | My Site' automatically appends the site name to every page title without repeating it in each page file. For pages whose metadata depends on dynamic data (like a blog post title fetched from an API), you export an async generateMetadata function instead. This function receives the same params and searchParams props as the page component. Next.js deduplicates fetch calls between generateMetadata and the page component, so fetching the same data in both functions incurs only one network request.
Example
// app/layout.tsx — root metadata with title template
import type { Metadata } from 'next';

export const metadata: Metadata = {
  // Title template: child pages set title, root appends '| Acme'
  title: {
    template: '%s | Acme',
    default: 'Acme — Build faster',
  },
  description: 'Acme helps you ship software faster.',
  metadataBase: new URL('https://acme.com'), // required for absolute OG URLs

  openGraph: {
    siteName: 'Acme',
    locale: 'en_US',
    type: 'website',
  },

  robots: {
    index: true,
    follow: true,
    googleBot: { index: true, follow: true },
  },
};

// app/blog/[slug]/page.tsx — dynamic metadata
import type { Metadata } from 'next';

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 },
  });
  if (!res.ok) return null;
  return res.json();
}

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug); // deduplicated with page fetch

  if (!post) return { title: 'Post not found' };

  return {
    title: post.title, // rendered as "Post Title | Acme"
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImageUrl, width: 1200, height: 630 }],
      type: 'article',
      publishedTime: post.publishedAt,
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImageUrl],
    },
  };
}