Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Nextjs
  4. /Dynamic Metadata
Your Progress0%
0 of 70 completed

NextJS Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication

Dynamic Metadata Generation

Creating metadata from page data and parameters

Static metadata works for fixed pages, but most apps need dynamic metadata that reflects current content. The generateMetadata function generates metadata based on route parameters, database content, or API data. It's async, type-safe, and runs automatically for every page. Build SEO-optimized pages that show current titles, descriptions, and images from your database!

Why Dynamic Metadata?

❌ Static Metadata Problem

TYPESCRIPT
// Static metadata doesn't know page content
export const metadata = {
  title: 'Blog Post',
  description: 'A blog post',
};

// Problems:
// ❌ Same title for all blog posts
// ❌ Generic description
// ❌ No actual content info
// ❌ Poor SEO
// ❌ Bad social previews

✅ Dynamic Metadata Solution

TYPESCRIPT
// Dynamic metadata from database
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  
  return {
    title: post.title,
    description: post.excerpt,
  };
}

// Benefits:
// ✅ Unique title per post
// ✅ Actual content description
// ✅ SEO optimized
// ✅ Rich social previews
// ✅ Always current

When to Use Dynamic Metadata

  • Blog posts from database
  • Product pages with current info
  • User profiles
  • Dynamic content routes
  • Search results pages
  • Any page where content determines metadata

generateMetadata Basics

Basic Example

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

interface Props {
  params: { slug: string };
  searchParams: { [key: string]: string | string[] | undefined };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  // Access route parameters
  const slug = params.slug;
  
  // Fetch data (can be async!)
  const post = await fetch(`https://api.example.com/posts/${slug}`)
    .then(res => res.json());
  
  // Return metadata object
  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default async function BlogPostPage({ params }: Props) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(res => res.json());
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

// ✅ Async metadata generation
// ✅ Access route parameters
// ✅ Fetch from any source
// ✅ Type-safe with TypeScript
// ✅ Automatic deduplication (fetch request cached)

generateMetadata Signature

TYPESCRIPT
export async function generateMetadata(
  { params, searchParams }: Props,
  parent?: ResolvingMetadata
): Promise<Metadata> {
  // params: Route parameters (e.g., { slug: 'my-post' })
  // searchParams: URL query params (e.g., { category: 'tech' })
  // parent: Parent metadata (optional, for extending)
  
  return {
    title: 'Dynamic Title',
    // ... other metadata
  };
}

// Returns: Metadata object
// Params: { params, searchParams }
// Optional: parent metadata for extending

Metadata from Database

Blog Post Example

app/lib/db.ts
// Database helper
export async function getPost(slug: string) {
  const post = await db.posts.findUnique({
    where: { slug },
    select: {
      id: true,
      title: true,
      content: true,
      excerpt: true,
      coverImage: true,
      author: {
        select: {
          name: true,
        },
      },
      publishedAt: true,
      tags: true,
    },
  });
  
  return post;
}

// ✅ Fetch only needed data
// ✅ Include author, tags for metadata
app/blog/[slug]/page.tsx
import { Metadata } from 'next';
import { getPost } from '@/app/lib/db';
import { notFound } from 'next/navigation';

interface Props {
  params: { slug: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug);
  
  if (!post) {
    return {
      title: 'Post Not Found',
    };
  }
  
  return {
    title: post.title,
    description: post.excerpt || post.content.substring(0, 160),
    
    authors: [{ name: post.author.name }],
    
    keywords: post.tags,
    
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt.toISOString(),
      authors: [post.author.name],
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
    
    alternates: {
      canonical: `https://myblog.com/blog/${params.slug}`,
    },
  };
}

export default async function BlogPostPage({ params }: Props) {
  const post = await getPost(params.slug);
  
  if (!post) {
    notFound();
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <img src={post.coverImage} alt={post.title} />
      <p>{post.content}</p>
    </article>
  );
}

// ✅ Complete metadata from database
// ✅ Handle not found case
// ✅ Open Graph for social media
// ✅ Twitter card
// ✅ Canonical URL with slug

Product Page Example

app/products/[id]/page.tsx
import { Metadata } from 'next';

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  images: string[];
  category: string;
  brand: string;
  inStock: boolean;
}

async function getProduct(id: string): Promise<Product | null> {
  const product = await db.products.findUnique({
    where: { id },
  });
  return product;
}

export async function generateMetadata({ 
  params 
}: { 
  params: { id: string } 
}): Promise<Metadata> {
  const product = await getProduct(params.id);
  
  if (!product) {
    return { title: 'Product Not Found' };
  }
  
  return {
    title: `${product.name} - ${product.brand}`,
    description: product.description,
    
    keywords: [
      product.name,
      product.brand,
      product.category,
      'buy online',
      'shop',
    ],
    
    openGraph: {
      title: product.name,
      description: product.description,
      type: 'product',
      images: product.images.map(img => ({
        url: img,
        width: 1200,
        height: 630,
        alt: product.name,
      })),
      // Product-specific Open Graph
      ...(product.price && {
        'product:price:amount': product.price.toString(),
        'product:price:currency': 'USD',
      }),
      ...(product.inStock !== undefined && {
        'product:availability': product.inStock ? 'in stock' : 'out of stock',
      }),
    },
    
    twitter: {
      card: 'summary_large_image',
      title: product.name,
      description: product.description,
      images: [product.images[0]],
    },
    
    alternates: {
      canonical: `https://mystore.com/products/${params.id}`,
    },
    
    // Structured data for rich snippets
    other: {
      'product:category': product.category,
      'product:brand': product.brand,
    },
  };
}

export default async function ProductPage({ 
  params 
}: { 
  params: { id: string } 
}) {
  const product = await getProduct(params.id);
  
  if (!product) {
    notFound();
  }
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <p>{product.description}</p>
    </div>
  );
}

// ✅ Product metadata from database
// ✅ Price and availability in Open Graph
// ✅ Multiple images
// ✅ Category and brand keywords

Extending Parent Metadata

Using Parent Metadata

app/blog/[slug]/page.tsx
import { Metadata, ResolvingMetadata } from 'next';

export async function generateMetadata(
  { params }: { params: { slug: string } },
  parent: ResolvingMetadata
): Promise<Metadata> {
  const post = await getPost(params.slug);
  
  // Access parent metadata
  const previousImages = (await parent).openGraph?.images || [];
  
  return {
    title: post.title,
    
    openGraph: {
      images: [
        // Add new image first
        post.coverImage,
        // Keep parent images as fallback
        ...previousImages,
      ],
    },
  };
}

// ✅ Extend parent metadata
// ✅ Access parent Open Graph images
// ✅ Combine with new metadata
// ✅ Fallback to parent

Merging with Parent

TYPESCRIPT
export async function generateMetadata(
  { params }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const post = await getPost(params.slug);
  const parentMetadata = await parent;
  
  return {
    title: post.title,
    description: post.excerpt,
    
    // Merge keywords
    keywords: [
      ...(parentMetadata.keywords || []),
      ...post.tags,
    ],
    
    // Extend Open Graph
    openGraph: {
      ...parentMetadata.openGraph,
      title: post.title,
      description: post.excerpt,
      type: 'article',
      images: [post.coverImage],
    },
  };
}

// ✅ Merge parent keywords
// ✅ Extend Open Graph
// ✅ Override specific fields
// ✅ Keep parent defaults

Using Search Parameters

Search Results Page

app/search/page.tsx
import { Metadata } from 'next';

interface Props {
  searchParams: { 
    q?: string;
    category?: string;
    page?: string;
  };
}

export async function generateMetadata({ 
  searchParams 
}: Props): Promise<Metadata> {
  const query = searchParams.q || '';
  const category = searchParams.category;
  const page = searchParams.page || '1';
  
  let title = 'Search Results';
  let description = 'Search our site';
  
  if (query) {
    title = `Search results for "${query}"`;
    description = `Find results for ${query} on our site`;
  }
  
  if (category) {
    title += ` in ${category}`;
    description += ` in ${category} category`;
  }
  
  if (page !== '1') {
    title += ` - Page ${page}`;
  }
  
  return {
    title,
    description,
    
    // Prevent indexing paginated pages
    robots: {
      index: page === '1',
      follow: true,
    },
    
    // Canonical to page 1
    alternates: {
      canonical: `https://example.com/search?q=${query}${
        category ? `&category=${category}` : ''
      }`,
    },
  };
}

export default async function SearchPage({ searchParams }: Props) {
  const query = searchParams.q || '';
  const results = await searchDatabase(query);
  
  return (
    <div>
      <h1>Search Results for "{query}"</h1>
      {/* Results */}
    </div>
  );
}

// ✅ Dynamic title from search query
// ✅ Include category and page
// ✅ Prevent indexing pagination
// ✅ Canonical to page 1

Category Filtering

app/products/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: { category?: string; sort?: string };
}): Promise<Metadata> {
  const category = searchParams.category;
  const sort = searchParams.sort;
  
  if (!category) {
    return {
      title: 'All Products',
      description: 'Browse all our products',
    };
  }
  
  // Capitalize category for display
  const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
  
  return {
    title: `${categoryName} Products`,
    description: `Shop our ${categoryName.toLowerCase()} products`,
    
    keywords: [category, 'products', 'buy', 'shop'],
    
    openGraph: {
      title: `${categoryName} Products`,
      description: `Shop our ${categoryName.toLowerCase()} products`,
    },
    
    // Only index default sort
    robots: {
      index: !sort || sort === 'default',
      follow: true,
    },
  };
}

// ✅ Dynamic title per category
// ✅ Prevent indexing sort variations
// ✅ Category-specific keywords

Error Handling in generateMetadata

Handling Missing Data

TYPESCRIPT
export async function generateMetadata({ 
  params 
}: Props): Promise<Metadata> {
  try {
    const post = await getPost(params.slug);
    
    if (!post) {
      // Return fallback metadata for 404
      return {
        title: 'Post Not Found',
        description: 'The blog post you are looking for does not exist.',
        robots: {
          index: false,
          follow: false,
        },
      };
    }
    
    return {
      title: post.title,
      description: post.excerpt,
      // ... other metadata
    };
  } catch (error) {
    console.error('Error generating metadata:', error);
    
    // Fallback metadata on error
    return {
      title: 'Error Loading Post',
      description: 'An error occurred while loading this post.',
      robots: {
        index: false,
        follow: true,
      },
    };
  }
}

// ✅ Handle not found
// ✅ Handle errors gracefully
// ✅ Prevent indexing error pages
// ✅ Return fallback metadata

Default Values and Fallbacks

TYPESCRIPT
export async function generateMetadata({ 
  params 
}: Props): Promise<Metadata> {
  const post = await getPost(params.slug);
  
  if (!post) {
    return { title: 'Not Found' };
  }
  
  return {
    // Use post title or fallback
    title: post.title || 'Untitled Post',
    
    // Use excerpt or truncated content
    description: 
      post.excerpt || 
      post.content?.substring(0, 160) || 
      'Read this blog post',
    
    // Use cover image or default
    openGraph: {
      images: [
        post.coverImage || 'https://example.com/default-og.jpg',
      ],
    },
    
    // Safe array access
    keywords: post.tags || [],
    
    // Format date safely
    ...(post.publishedAt && {
      openGraph: {
        publishedTime: post.publishedAt.toISOString(),
      },
    }),
  };
}

// ✅ Fallback for missing fields
// ✅ Default images
// ✅ Safe property access
// ✅ Conditional metadata

Performance Optimization

Request Deduplication

TYPESCRIPT
// Next.js automatically deduplicates identical requests
export async function generateMetadata({ params }: Props) {
  // This request is cached and shared
  const post = await fetch(`/api/posts/${params.slug}`);
  return { title: post.title };
}

export default async function Page({ params }: Props) {
  // Same request - uses cached result!
  const post = await fetch(`/api/posts/${params.slug}`);
  return <div>{post.content}</div>;
}

// ✅ Automatic request deduplication
// ✅ Same data fetched once
// ✅ No extra database queries
// ✅ Better performance

Parallel Metadata Generation

TYPESCRIPT
export async function generateMetadata({ params }: Props) {
  // Fetch multiple things in parallel
  const [post, author, relatedPosts] = await Promise.all([
    getPost(params.slug),
    getAuthor(params.authorId),
    getRelatedPosts(params.slug),
  ]);
  
  return {
    title: post.title,
    authors: [{ name: author.name }],
    description: post.excerpt,
    // Use relatedPosts for keywords, etc.
  };
}

// ✅ Parallel fetching
// ✅ Faster metadata generation
// ✅ Reduced wait time

Caching Strategy

TYPESCRIPT
// Cache metadata generation
export async function generateMetadata({ params }: Props) {
  const post = await fetch(`/api/posts/${params.slug}`, {
    next: {
      revalidate: 3600, // Cache for 1 hour
      tags: ['posts', `post-${params.slug}`],
    },
  }).then(res => res.json());
  
  return {
    title: post.title,
    description: post.excerpt,
  };
}

// ✅ Cache metadata fetches
// ✅ Revalidate periodically
// ✅ Tag-based invalidation
// ✅ Better performance

Dynamic Metadata Structure

Organization of pages with generateMetadata

appImportant

Select a file or folder to see details

Dynamic Metadata Best Practices

1. Always Handle Missing Data

TYPESCRIPT
// ✅ GOOD: Handle not found
export async function generateMetadata({ params }: Props) {
  const post = await getPost(params.slug);
  
  if (!post) {
    return {
      title: 'Not Found',
      robots: { index: false },
    };
  }
  
  return { title: post.title };
}

// ❌ BAD: Assumes data exists
export async function generateMetadata({ params }: Props) {
  const post = await getPost(params.slug);
  return { title: post.title }; // Crashes if post is null!
}

2. Use Same Data Fetching as Page

TYPESCRIPT
// ✅ GOOD: Share fetching logic
async function getPost(slug: string) {
  return await db.posts.findUnique({ where: { slug } });
}

export async function generateMetadata({ params }: Props) {
  const post = await getPost(params.slug);
  return { title: post.title };
}

export default async function Page({ params }: Props) {
  const post = await getPost(params.slug);
  return <div>{post.content}</div>;
}

// Requests automatically deduplicated

3. Provide Meaningful Fallbacks

TYPESCRIPT
// ✅ GOOD: Meaningful fallbacks
return {
  title: post.title || 'Untitled Post',
  description: post.excerpt || 'Read this blog post',
  openGraph: {
    images: [post.image || '/default-og.jpg'],
  },
};

// ❌ BAD: Empty or generic fallbacks
return {
  title: post.title || 'Post',
  description: post.excerpt || '',
};

4. Don't Index Duplicate Content

TYPESCRIPT
// ✅ GOOD: Prevent indexing duplicates
export async function generateMetadata({ searchParams }: Props) {
  const page = searchParams.page || '1';
  
  return {
    title: `Search Results - Page ${page}`,
    robots: {
      index: page === '1', // Only index first page
      follow: true,
    },
    alternates: {
      canonical: 'https://example.com/search', // Canonical to page 1
    },
  };
}

5. Keep Metadata Generation Fast

TYPESCRIPT
// ✅ GOOD: Fast, focused queries
export async function generateMetadata({ params }: Props) {
  const post = await db.posts.findUnique({
    where: { slug: params.slug },
    select: {
      title: true,
      excerpt: true,
      coverImage: true,
      // Only select what's needed
    },
  });
  
  return { title: post.title };
}

// ❌ BAD: Slow, unfocused queries
export async function generateMetadata({ params }: Props) {
  const post = await db.posts.findUnique({
    where: { slug: params.slug },
    include: {
      author: true,
      comments: true, // Not needed for metadata!
      relatedPosts: true, // Not needed!
    },
  });
  
  return { title: post.title };
}

Key Takeaways

  • generateMetadata - async function for dynamic metadata
  • Route parameters - access via params
  • Search parameters - access via searchParams
  • Database content - fetch and use for metadata
  • Parent metadata - extend with parent parameter
  • Error handling - always handle missing data
  • Request deduplication - automatic caching
  • Type safety - full TypeScript support

What's Next?

You've mastered dynamic metadata generation! Next, we'll explore Open Graph and Social Media Cards—creating dynamic OG images, customizing social media previews, and building rich cards for Twitter, Facebook, and LinkedIn. You'll make your content shine when shared!

We'll cover dynamic OG image generation, social media best practices, preview debugging, and creating engaging social cards that drive clicks.

⚡ Performance Matters

generateMetadata runs for every page request in dynamic routes. Keep it fast! Fetch only needed fields, use caching, and leverage automatic request deduplication. Fast metadata generation means faster page loads.

Test Your Understanding

Question 1 of 4

What is generateMetadata used for?

Master dynamic metadata in Next.js! Learn generateMetadata for SEO-optimized, database-driven metadata.

Previous
Static Metadata Configuration
Next
Open Graph and Social Media Cards

Master Next.js SEO

Join 2,000+ developers building SEO-optimized Next.js apps. Get the next lesson on Open Graph and social cards - 100% FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

NextJS Tutorials

0 of 70 completed

Your Progress0%

Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo