React’s Server Components, while powerful, can make dynamically updating meta tags a bit of a puzzle, especially when you’re used to the client-side rendering approach with react-helmet.

Here’s how to nail dynamic meta tags in Next.js App Router using next/head (the successor to react-helmet in this context).

Let’s say you have a blog post page at /posts/[slug]. You want the meta title and description to reflect the actual post content.

// app/posts/[slug]/page.js
import { getPostBySlug } from '@/lib/posts'; // Assume this fetches your post data

async function BlogPostPage({ params }) {
  const post = await getPostBySlug(params.slug);

  if (!post) {
    return <div>Post not found</div>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

export default BlogPostPage;

To add dynamic meta tags, you’ll use the generateMetadata function. This function runs on the server and its return value is used by Next.js to generate the <head> section of your HTML.

// app/posts/[slug]/page.js
import { getPostBySlug } from '@/lib/posts';

// This function runs on the server
export async function generateMetadata({ params }) {
  const post = await getPostBySlug(params.slug);

  if (!post) {
    return {
      title: 'Post Not Found',
      description: 'The requested blog post could not be found.',
    };
  }

  return {
    title: post.title,
    description: post.excerpt || post.content.substring(0, 150) + '...', // Use excerpt or a snippet
    openGraph: {
      title: post.title,
      description: post.excerpt || post.content.substring(0, 150) + '...',
      images: [post.imageUrl || '/default-og-image.png'], // Add an image for social sharing
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt || post.content.substring(0, 150) + '...',
      image: post.imageUrl || '/default-og-image.png',
    },
  };
}

async function BlogPostPage({ params }) {
  const post = await getPostBySlug(params.slug);

  if (!post) {
    return <div>Post not found</div>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

export default BlogPostPage;

The generateMetadata function can be defined directly in your page file. It receives the same props as your page component (params, searchParams, etc.), allowing you to fetch data relevant to the current page and use it to construct your metadata.

The returned object from generateMetadata maps directly to common meta tags, including title, description, and structured data for social media like openGraph and twitter. Next.js automatically handles rendering these into the correct <meta> and <title> tags in the HTML.

Consider this scenario: a user navigates to /posts/my-first-post. If getPostBySlug('my-first-post') returns a post with title: "My First Awesome Post" and excerpt: "This is the beginning of my blogging journey.", the generated HTML will include:

<title>My First Awesome Post</title>
<meta name="description" content="This is the beginning of my blogging journey.">
<meta property="og:title" content="My First Awesome Post">
<meta property="og:description" content="This is the beginning of my blogging journey.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="My First Awesome Post">
<meta name="twitter:description" content="This is the beginning of my blogging journey.">

This server-side generation is crucial for SEO because search engine crawlers and social media bots can read this directly from the initial HTML response, unlike client-side rendered meta tags which might not be present until JavaScript has executed.

When you start dealing with nested routes, like /products/[category]/[id], you can define generateMetadata at multiple levels. Metadata from parent layouts and pages is merged with metadata from child pages. If there’s a conflict (e.g., two title tags), the more specific child route’s metadata usually takes precedence.

For example, you might have a layout.js in app/products/[category] that sets a base title like "Products in [Category Name]", and then page.js for [id] overrides it with the specific product name.

The generateMetadata function can also return a Promise, making it ideal for asynchronous data fetching, as shown in the example. This ensures that even complex metadata dependent on dynamic data is ready before the page is sent to the client.

One subtle but powerful aspect is how generateMetadata interacts with the App Router’s layout system. You can define generateMetadata in layout.js files as well as page.js files. Next.js merges these metadata objects. If a title is defined in both a layout and a page, the title from the page component will be used, providing a clear hierarchy of specificity. This allows you to set global site metadata in root layouts and then override or augment it in specific sections or pages.

The generateMetadata function is your primary tool. It’s not just about setting basic tags; you can configure robots directives (robots: { index: false, follow: true }), canonical URLs (alternates: { canonical: '/my-canonical-url' }), and much more, all server-side.

The next hurdle you’ll likely encounter is managing dynamic metadata for routes that don’t have static slugs, like search results pages or dynamically generated category listings where you can’t use generateStaticParams.

Want structured learning?

Take the full React course →