Next.js Metadata and SEO

Generate metadata, open graph tags, and improve SEO automatically.

TL;DR

  1. 01Export metadata constant to set page titles and descriptions.
  2. 02Use generateMetadata for dynamic metadata from data.
  3. 03Use Open Graph tags for social media sharing.

Tips

  1. 01Use generateMetadata with dynamic data to create unique, SEO-friendly titles and descriptions for each page.

Warnings

  1. 01Always include Open Graph images with correct dimensions (1200x630) to ensure proper display on social media.

Static Metadata

  • Export metadata constant for static pages.
    import { Metadata } from "next";
    
    export const metadata: Metadata = {
      title: "About Us",
      description: "Learn more about our company and mission.",
      keywords: ["about", "company", "mission"]
    };
    
    export default function About() {
      return <div>About content</div>;
    }
  • Metadata sets HTML head tags automatically.
  • Improves SEO and social media sharing.
  • Use a layout-level metadata object to apply defaults across all pages.
    // app/layout.tsx
    export const metadata: Metadata = {
      title: { default: "My Site", template: "%s | My Site" },
      description: "Default description for all pages."
    };
  • Child page metadata merges with and overrides layout metadata.
    // app/about/page.tsx
    export const metadata: Metadata = {
      title: "About" // becomes "About | My Site" via template
    };

Dynamic Metadata

  • Generate metadata from data or parameters.
    import { Metadata } from "next";
    
    export async function generateMetadata({
      params
    }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
      const { slug } = await params; // params is a Promise in Next.js 15
      const post = await getPost(slug);
      
      return {
        title: post.title,
        description: post.excerpt,
        authors: [{ name: post.author }]
      };
    }
    
    export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params;
      const post = await getPost(slug);
      return <article>{post.content}</article>;
    }
  • generateMetadata runs on the server.
  • Has access to params and other request data.
  • Fetch and cache data inside generateMetadata to avoid duplicate requests.
    export async function generateMetadata({ params }): Promise<Metadata> {
      const product = await getProduct(params.id); // Next.js deduplicates this fetch
      return { title: product.name, description: product.description };
    }
    export default async function Page({ params }) {
      const product = await getProduct(params.id); // same fetch, cached
      return <ProductDetail product={product} />;
    }
  • Return notFound() inside generateMetadata to trigger a 404 page early.
    export async function generateMetadata({ params }): Promise<Metadata> {
      const post = await getPost(params.slug);
      if (!post) notFound();
      return { title: post.title };
    }

Open Graph Tags

  • Set Open Graph tags for social sharing.
    export const metadata: Metadata = {
      title: "My Post",
      description: "Read my latest blog post",
      openGraph: {
        title: "My Post",
        description: "Read my latest blog post",
        url: "https://example.com/blog/my-post",
        siteName: "My Blog",
        images: [
          {
            url: "https://example.com/og-image.png",
            width: 1200,
            height: 630
          }
        ],
        type: "article"
      }
    };
  • Open Graph improves how links look on social media.
  • Include images for better engagement.
  • Use dynamic OG images with Next.js ImageResponse for per-page previews.
    // app/og/route.tsx
    import { ImageResponse } from "next/og";
    export async function GET(req: Request) {
      const { searchParams } = new URL(req.url);
      return new ImageResponse(<div>{searchParams.get("title")}</div>);
    }
  • Reference the dynamic OG route in your metadata image field.
    openGraph: {
      images: [`/og?title=${encodeURIComponent(post.title)}`]
    }

Twitter Card Tags

  • Add Twitter Card metadata for tweets.
    export const metadata: Metadata = {
      title: "My Post",
      description: "Read my latest blog post",
      twitter: {
        card: "summary_large_image",
        title: "My Post",
        description: "Read my latest blog post",
        images: ["https://example.com/og-image.png"],
        creator: "@myhandle"
      }
    };
  • Twitter Cards make tweets more engaging.
  • Use summary_large_image for best results.
  • Set twitter.site to your handle for attribution on shared links.
    twitter: {
      card: "summary_large_image",
      site: "@mycompany",
      creator: "@authorhandle"
    }
  • Reuse the same image URL for both OG and Twitter to reduce duplication.
    const ogImage = "https://example.com/og-image.png";
    export const metadata: Metadata = {
      openGraph: { images: [ogImage] },
      twitter: { images: [ogImage] }
    };

Structured Data

  • Add JSON-LD structured data for search engines.
    export default async function BlogPost({
      params
    }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params;
      const post = await getPost(slug);
      
      const schema = {
        "@context": "https://schema.org",
        "@type": "BlogPosting",
        headline: post.title,
        description: post.excerpt,
        image: post.image,
        author: {
          "@type": "Person",
          name: post.author
        },
        datePublished: post.date
      };
      
      return (
        <>
          <script
            type="application/ld+json"
            dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
          />
          <article>{post.content}</article>
        </>
      );
    }
  • Structured data helps search engines understand content.
  • Use schema.org types for consistency.
  • Add BreadcrumbList schema to help search engines show site hierarchy.
    const breadcrumb = {
      "@context": "https://schema.org",
      "@type": "BreadcrumbList",
      itemListElement: [
        { "@type": "ListItem", position: 1, name: "Home", item: "/" },
        { "@type": "ListItem", position: 2, name: "Blog", item: "/blog" }
      ]
    };
  • Validate structured data with Google's Rich Results Test tool before deploying.

FAQ