Next.js Metadata Cheat Sheet

Basic Metadata Setup

Root Layout Metadata

// app/layout.js
export const metadata = {
    title: {
        default: 'My Website',
        template: '%s | My Website'
    },
    description: 'A comprehensive guide to Next.js development',
    keywords: ['Next.js', 'React', 'JavaScript', 'Web Development'],
    authors: [{ name: 'John Doe' }],
    creator: 'John Doe',
    publisher: 'My Company',
    formatDetection: {
        email: false,
        address: false,
        telephone: false,
    },
    metadataBase: new URL('https://example.com'),
    alternates: {
        canonical: '/',
    },
    robots: {
        index: true,
        follow: true,
        googleBot: {
            index: true,
            follow: true,
            'max-video-preview': -1,
            'max-image-preview': 'large',
            'max-snippet': -1,
        },
    },
};

export default function RootLayout({ children }) {
    return (
        <html lang="en">
            <body>{children}</body>
        </html>
    );
}

Page-Level Metadata

// app/about/page.js
export const metadata = {
    title: 'About Us',
    description: 'Learn more about our company and mission',
    keywords: ['about', 'company', 'mission'],
    openGraph: {
        title: 'About Us',
        description: 'Learn more about our company and mission',
        url: 'https://example.com/about',
        siteName: 'My Website',
        images: [
            {
                url: 'https://example.com/og-image.jpg',
                width: 1200,
                height: 630,
                alt: 'About Us',
            },
        ],
        locale: 'en_US',
        type: 'website',
    },
    twitter: {
        card: 'summary_large_image',
        title: 'About Us',
        description: 'Learn more about our company and mission',
        images: ['https://example.com/twitter-image.jpg'],
    },
};

export default function AboutPage() {
    return <div>About page content</div>;
}

Dynamic Metadata

Generate Metadata Function

// app/blog/[slug]/page.js
export async function generateMetadata({ params, searchParams }) {
    // Fetch data based on params
    const post = await getPost(params.slug);
    
    if (!post) {
        return {
            title: 'Post Not Found',
        };
    }
    
    return {
        title: post.title,
        description: post.excerpt,
        keywords: post.tags,
        openGraph: {
            title: post.title,
            description: post.excerpt,
            url: `https://example.com/blog/${params.slug}`,
            images: [
                {
                    url: post.featuredImage,
                    width: 1200,
                    height: 630,
                    alt: post.title,
                },
            ],
            type: 'article',
            publishedTime: post.publishedAt,
            authors: [post.author],
            tags: post.tags,
        },
        twitter: {
            card: 'summary_large_image',
            title: post.title,
            description: post.excerpt,
            images: [post.featuredImage],
        },
    };
}

Metadata with Search Params

// app/search/page.js
export async function generateMetadata({ searchParams }) {
    const query = searchParams.q || '';
    
    return {
        title: query ? `Search Results for "${query}"` : 'Search',
        description: query 
            ? `Search results for "${query}" on our website`
            : 'Search our website for articles, products, and more',
        robots: {
            index: false, // Don't index search pages
            follow: true,
        },
    };
}

Open Graph Metadata

Basic Open Graph

export const metadata = {
    openGraph: {
        title: 'Page Title',
        description: 'Page description',
        url: 'https://example.com/page',
        siteName: 'My Website',
        images: [
            {
                url: 'https://example.com/image.jpg',
                width: 1200,
                height: 630,
                alt: 'Image description',
            },
        ],
        locale: 'en_US',
        type: 'website',
    },
};

Article Open Graph

export const metadata = {
    openGraph: {
        type: 'article',
        title: 'Article Title',
        description: 'Article description',
        url: 'https://example.com/article',
        siteName: 'My Website',
        images: [
            {
                url: 'https://example.com/article-image.jpg',
                width: 1200,
                height: 630,
                alt: 'Article featured image',
            },
        ],
        publishedTime: '2024-01-15T10:00:00Z',
        modifiedTime: '2024-01-16T15:30:00Z',
        authors: ['John Doe', 'Jane Smith'],
        tags: ['technology', 'programming', 'nextjs'],
        section: 'Technology',
    },
};

Product Open Graph

export const metadata = {
    openGraph: {
        type: 'product',
        title: 'Product Name',
        description: 'Product description',
        url: 'https://example.com/product',
        siteName: 'My Store',
        images: [
            {
                url: 'https://example.com/product-image.jpg',
                width: 1200,
                height: 630,
                alt: 'Product image',
            },
        ],
        price: {
            amount: '99.99',
            currency: 'USD',
        },
        availability: 'in stock',
        brand: 'Brand Name',
        category: 'Electronics',
    },
};

Twitter Card Metadata

Summary Card

export const metadata = {
    twitter: {
        card: 'summary',
        title: 'Page Title',
        description: 'Page description',
        images: ['https://example.com/image.jpg'],
        creator: '@username',
        site: '@sitehandle',
    },
};

Large Image Card

export const metadata = {
    twitter: {
        card: 'summary_large_image',
        title: 'Page Title',
        description: 'Page description',
        images: ['https://example.com/large-image.jpg'],
        creator: '@username',
        site: '@sitehandle',
    },
};

App Card

export const metadata = {
    twitter: {
        card: 'app',
        title: 'App Name',
        description: 'App description',
        images: ['https://example.com/app-icon.jpg'],
        app: {
            name: {
                iphone: 'App Name',
                ipad: 'App Name',
                googleplay: 'App Name',
            },
            id: {
                iphone: '123456789',
                ipad: '123456789',
                googleplay: 'com.example.app',
            },
            url: {
                iphone: 'https://apps.apple.com/app/id123456789',
                ipad: 'https://apps.apple.com/app/id123456789',
                googleplay: 'https://play.google.com/store/apps/details?id=com.example.app',
            },
        },
    },
};

Structured Data (JSON-LD)

Article Structured Data

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
    const post = await getPost(params.slug);
    
    const jsonLd = {
        '@context': 'https://schema.org',
        '@type': 'Article',
        headline: post.title,
        description: post.excerpt,
        image: post.featuredImage,
        author: {
            '@type': 'Person',
            name: post.author,
        },
        publisher: {
            '@type': 'Organization',
            name: 'My Website',
            logo: {
                '@type': 'ImageObject',
                url: 'https://example.com/logo.png',
            },
        },
        datePublished: post.publishedAt,
        dateModified: post.updatedAt,
        mainEntityOfPage: {
            '@type': 'WebPage',
            '@id': `https://example.com/blog/${params.slug}`,
        },
    };
    
    return (
        <>
            <script
                type="application/ld+json"
                dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
            />
            <article>
                <h1>{post.title}</h1>
                <div>{post.content}</div>
            </article>
        </>
    );
}

Organization Structured Data

// app/layout.js
export default function RootLayout({ children }) {
    const organizationSchema = {
        '@context': 'https://schema.org',
        '@type': 'Organization',
        name: 'My Company',
        url: 'https://example.com',
        logo: 'https://example.com/logo.png',
        sameAs: [
            'https://twitter.com/mycompany',
            'https://linkedin.com/company/mycompany',
            'https://facebook.com/mycompany',
        ],
        contactPoint: {
            '@type': 'ContactPoint',
            telephone: '+1-555-123-4567',
            contactType: 'customer service',
        },
    };
    
    return (
        <html lang="en">
            <head>
                <script
                    type="application/ld+json"
                    dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
                />
            </head>
            <body>{children}</body>
        </html>
    );
}

SEO Optimization

Canonical URLs

export const metadata = {
    alternates: {
        canonical: 'https://example.com/page',
        languages: {
            'en-US': 'https://example.com/en-US/page',
            'es-ES': 'https://example.com/es-ES/page',
        },
    },
};

Robots Meta Tags

export const metadata = {
    robots: {
        index: true,
        follow: true,
        nocache: true,
        googleBot: {
            index: true,
            follow: true,
            noimageindex: true,
            'max-video-preview': -1,
            'max-image-preview': 'large',
            'max-snippet': -1,
        },
    },
};

Sitemap Generation

// app/sitemap.js
export default async function sitemap() {
    const baseUrl = 'https://example.com';
    
    // Get all blog posts
    const posts = await getAllPosts();
    const blogUrls = posts.map((post) => ({
        url: `${baseUrl}/blog/${post.slug}`,
        lastModified: post.updatedAt,
        changeFrequency: 'weekly',
        priority: 0.7,
    }));
    
    // Static pages
    const staticPages = [
        {
            url: baseUrl,
            lastModified: new Date(),
            changeFrequency: 'daily',
            priority: 1,
        },
        {
            url: `${baseUrl}/about`,
            lastModified: new Date(),
            changeFrequency: 'monthly',
            priority: 0.8,
        },
        {
            url: `${baseUrl}/contact`,
            lastModified: new Date(),
            changeFrequency: 'monthly',
            priority: 0.5,
        },
    ];
    
    return [...staticPages, ...blogUrls];
}

Robots.txt

// app/robots.js
export default function robots() {
    return {
        rules: [
            {
                userAgent: '*',
                allow: '/',
                disallow: ['/private/', '/admin/', '/api/'],
            },
        ],
        sitemap: 'https://example.com/sitemap.xml',
    };
}

Advanced Metadata Patterns

Conditional Metadata

// app/products/[id]/page.js
export async function generateMetadata({ params }) {
    const product = await getProduct(params.id);
    
    if (!product) {
        return {
            title: 'Product Not Found',
            robots: {
                index: false,
                follow: false,
            },
        };
    }
    
    const metadata = {
        title: product.name,
        description: product.description,
        openGraph: {
            title: product.name,
            description: product.description,
            images: [product.image],
            type: 'product',
        },
    };
    
    // Add price if available
    if (product.price) {
        metadata.openGraph.price = {
            amount: product.price.toString(),
            currency: 'USD',
        };
    }
    
    // Add availability
    if (product.inStock) {
        metadata.openGraph.availability = 'in stock';
    } else {
        metadata.openGraph.availability = 'out of stock';
    }
    
    return metadata;
}

Metadata with API Data

// app/dashboard/page.js
export async function generateMetadata() {
    // Fetch user data for personalized metadata
    const user = await getCurrentUser();
    
    return {
        title: user ? `${user.name}'s Dashboard` : 'Dashboard',
        description: user 
            ? `Welcome back, ${user.name}! Manage your account and preferences.`
            : 'Access your personalized dashboard.',
        robots: {
            index: false, // Don't index user-specific pages
            follow: false,
        },
    };
}

Metadata for Different Environments

// lib/metadata.js
export function getBaseMetadata() {
    const isProduction = process.env.NODE_ENV === 'production';
    const baseUrl = isProduction 
        ? 'https://example.com' 
        : 'http://localhost:3000';
    
    return {
        metadataBase: new URL(baseUrl),
        robots: {
            index: isProduction,
            follow: isProduction,
        },
    };
}

// app/layout.js
import { getBaseMetadata } from '@/lib/metadata';

export const metadata = {
    ...getBaseMetadata(),
    title: {
        default: 'My Website',
        template: '%s | My Website'
    },
    description: 'Website description',
};

Performance Optimization

Metadata Caching

// lib/metadata-cache.js
const metadataCache = new Map();

export async function getCachedMetadata(key, fetcher) {
    if (metadataCache.has(key)) {
        return metadataCache.get(key);
    }
    
    const metadata = await fetcher();
    metadataCache.set(key, metadata);
    
    return metadata;
}

// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
    return getCachedMetadata(`post-${params.slug}`, async () => {
        const post = await getPost(params.slug);
        return {
            title: post.title,
            description: post.excerpt,
            // ... other metadata
        };
    });
}

Lazy Metadata Loading

// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
    // Only fetch essential metadata initially
    const post = await getPostBasic(params.slug);
    
    const metadata = {
        title: post.title,
        description: post.excerpt,
    };
    
    // Load additional metadata in parallel
    const [fullPost, author] = await Promise.all([
        getPostFull(params.slug),
        getAuthor(post.authorId),
    ]);
    
    // Enhance metadata with additional data
    return {
        ...metadata,
        openGraph: {
            title: fullPost.title,
            description: fullPost.excerpt,
            images: [fullPost.featuredImage],
            authors: [author.name],
        },
    };
}

Best Practices

Metadata Validation

// lib/validate-metadata.js
export function validateMetadata(metadata) {
    const errors = [];
    
    if (!metadata.title) {
        errors.push('Title is required');
    }
    
    if (!metadata.description) {
        errors.push('Description is required');
    }
    
    if (metadata.description && metadata.description.length > 160) {
        errors.push('Description should be under 160 characters');
    }
    
    if (metadata.openGraph?.images) {
        metadata.openGraph.images.forEach((image, index) => {
            if (!image.url) {
                errors.push(`Open Graph image ${index} is missing URL`);
            }
            if (!image.alt) {
                errors.push(`Open Graph image ${index} is missing alt text`);
            }
        });
    }
    
    return errors;
}

Metadata Testing

// __tests__/metadata.test.js
import { generateMetadata } from '../app/blog/[slug]/page';

describe('Blog Post Metadata', () => {
    it('should generate correct metadata for valid post', async () => {
        const metadata = await generateMetadata({ 
            params: { slug: 'test-post' } 
        });
        
        expect(metadata.title).toBeDefined();
        expect(metadata.description).toBeDefined();
        expect(metadata.openGraph).toBeDefined();
        expect(metadata.openGraph.title).toBe(metadata.title);
    });
    
    it('should handle missing posts gracefully', async () => {
        const metadata = await generateMetadata({ 
            params: { slug: 'non-existent' } 
        });
        
        expect(metadata.title).toBe('Post Not Found');
        expect(metadata.robots.index).toBe(false);
    });
});