Next.js Metadata API — SEO Best Practices

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Next.js Metadata API — SEO Best Practices

The Metadata API provides a declarative way to define SEO-friendly metadata for your pages without worrying about HTML head tag management.

Basic Metadata

Define metadata in any page or layout file:

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

export const metadata: Metadata = {
  title: 'Home - My Blog',
  description: 'Welcome to my tech blog with articles on Next.js and React.',
}

export default function HomePage() {
  return <div>Home page content</div>
}

Next.js automatically adds these to the HTML head.

Dynamic Metadata

Generate metadata dynamically:

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

async function getPost(slug: string) {
  const post = await db.posts.findUnique({ where: { slug } })
  return post
}

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug)

  if (!post) {
    return {
      title: 'Post Not Found',
    }
  }

  return {
    title: post.title,
    description: post.excerpt,
    authors: [{ name: post.author }],
    keywords: post.tags,
  }
}

export default async function PostPage({ params }) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

Open Graph Metadata

Add social media sharing metadata:

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

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `https://example.com/blog/${post.slug}`,
      siteName: 'My Blog',
      images: [
        {
          url: post.imageUrl,
          width: 1200,
          height: 630,
          alt: post.title,
        }
      ],
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.imageUrl],
      creator: '@yourhandle',
    },
  }
}

export default async function PostPage({ params }) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

Canonical URLs

Prevent duplicate content issues:

// app/products/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Products',
  canonical: 'https://example.com/products',
  alternates: {
    canonical: 'https://example.com/products',
    languages: {
      es: 'https://example.com/es/products',
      fr: 'https://example.com/fr/products',
    },
  },
}

export default function ProductsPage() {
  return <div>Products list</div>
}

Robots and Indexing

Control search engine crawling:

// app/admin/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  robots: {
    index: false, // Don't index this page
    follow: false, // Don't follow links
    nocache: true,
  },
}

export default function AdminPage() {
  return <div>Admin dashboard</div>
}

Structured Data (JSON-LD)

Add structured data for rich snippets:

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

export async function generateMetadata({
  params,
}: {
  params: { id: string }
}): Promise<Metadata> {
  const product = await getProduct(params.id)

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.description,
    image: product.image,
    price: product.price,
    ratingValue: product.rating,
    reviewCount: product.reviewCount,
  }

  return {
    title: product.name,
    description: product.description,
    other: {
      'json-ld': JSON.stringify(jsonLd),
    },
  }
}

export default function ProductPage({ params }) {
  const product = getProduct(params.id)
  return <div>{product.name}</div>
}

Layout Metadata Merging

Metadata cascades through layouts:

// app/layout.tsx (Root layout)
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    template: '%s | My Blog',
    default: 'My Blog',
  },
  description: 'Tech blog about web development',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://example.com',
    siteName: 'My Blog',
  },
}

export default function RootLayout({ children }) {
  return <html>{children}</html>
}
// app/blog/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Blog',
}

export default function BlogLayout({ children }) {
  return <div>{children}</div>
}

When viewing /blog, the title becomes "Blog | My Blog" from the template.

Icons and App Manifest

Define app icons and manifest:

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'My awesome application',
  icons: {
    icon: '/favicon.ico',
    shortcut: '/favicon.ico',
    apple: '/apple-icon.png',
  },
  appleWebApp: {
    capable: true,
    statusBarStyle: 'black-translucent',
  },
  manifest: '/manifest.json',
}

export default function RootLayout({ children }) {
  return <html>{children}</html>
}

Viewport Settings

Configure viewport behavior:

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  viewport: {
    width: 'device-width',
    initialScale: 1,
    maximumScale: 1,
  },
}

export default function RootLayout({ children }) {
  return <html>{children}</html>
}

Real-World Example: Blog Post SEO

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

async function getPost(slug: string) {
  return db.posts.findUnique({
    where: { slug },
    include: { author: true, tags: true }
  })
}

export async function generateMetadata({
  params
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug)

  if (!post) {
    return { title: 'Not Found' }
  }

  const url = `https://example.com/blog/${post.slug}`

  return {
    title: post.title,
    description: post.excerpt,
    authors: [{ name: post.author.name, url: post.author.website }],
    keywords: post.tags.map(t => t.name),
    canonical: url,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url,
      type: 'article',
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      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],
    },
  }
}

export default async function BlogPostPage({ params }) {
  const post = await getPost(params.slug)

  const schema = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      '@type': 'Person',
      name: post.author.name,
    },
  }

  return (
    <article>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
      />
      <h1>{post.title}</h1>
      <p>{post.excerpt}</p>
      <div>{post.content}</div>
    </article>
  )
}

FAQ

Q: How do I check if my metadata is correct? A: Use Google's Rich Results Test or Meta's Sharing Debugger to validate your metadata.

Q: Can I change metadata based on user preferences? A: Metadata is generated server-side, so you can check user preferences but can't use client-side state.

Q: What about meta descriptions length? A: Keep descriptions between 120-160 characters for optimal display in search results.


Proper SEO metadata significantly improves search engine visibility and social sharing.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro