Next.js Cheatsheet

Metadata and SEO

Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Static Metadata Export

Export a metadata object from any page.tsx or layout.tsx:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Description for search engines.',
}

Metadata in a layout.tsx applies as a default to all nested routes; page.tsx metadata overrides it.

Dynamic Metadata — generateMetadata()

import type { Metadata } from 'next'

export async function generateMetadata({
  params,
  searchParams,
}: {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ [key: string]: string | undefined }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await fetchPost(slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.ogImage, width: 1200, height: 630 }],
    },
  }
}

Title Templates

Set a template in the root layout so all child pages auto-format titles:

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    template: '%s | My App',   // child title replaces %s
    default: 'My App',         // fallback when no child sets a title
  },
}

// app/about/page.tsx
export const metadata: Metadata = {
  title: 'About',   // renders as "About | My App"
}

absolute bypasses the template:

export const metadata: Metadata = {
  title: { absolute: 'Full Override Title' },  // ignores template
}

Full Metadata Fields Reference

export const metadata: Metadata = {
  // Basic
  title: 'Page Title',
  description: 'Meta description (≤160 chars ideal)',
  keywords: ['next.js', 'react', 'web'],
  authors: [{ name: 'Alice', url: 'https://alice.dev' }],
  creator: 'Alice',
  publisher: 'My Company',

  // Canonical / alternate
  alternates: {
    canonical: 'https://example.com/about',
    languages: {
      'en-US': 'https://example.com/en-US/about',
      'fr-FR': 'https://example.com/fr-FR/about',
    },
  },

  // Robots
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },

  // Open Graph
  openGraph: {
    title: 'OG Title',
    description: 'OG description',
    url: 'https://example.com/about',
    siteName: 'My App',
    images: [
      {
        url: 'https://example.com/og.png',
        width: 1200,
        height: 630,
        alt: 'My App homepage',
      },
    ],
    locale: 'en_US',
    type: 'website',   // 'article' | 'video.movie' | 'profile' | etc.
  },

  // Twitter / X Card
  twitter: {
    card: 'summary_large_image',
    title: 'Twitter Title',
    description: 'Twitter description',
    creator: '@username',
    images: ['https://example.com/og.png'],
  },

  // Favicon / Icons
  icons: {
    icon: '/favicon.ico',
    shortcut: '/shortcut-icon.png',
    apple: '/apple-touch-icon.png',
    other: [
      { rel: 'icon', url: '/icon-32x32.png', sizes: '32x32', type: 'image/png' },
    ],
  },

  // Web manifest
  manifest: '/manifest.json',

  // Verification (search console, etc.)
  verification: {
    google: 'google-site-verification-token',
    yandex: 'yandex-token',
    yahoo: 'yahoo-token',
  },

  // Viewport (use generateViewport instead — see below)
  // viewport is deprecated in favor of the generateViewport export
}

generateViewport

import type { Viewport } from 'next'

export const viewport: Viewport = {
  width: 'device-width',
  initialScale: 1,
  maximumScale: 1,
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: '#ffffff' },
    { media: '(prefers-color-scheme: dark)', color: '#000000' },
  ],
  colorScheme: 'light dark',
}

Dynamic viewport:

export async function generateViewport({ params }: Props): Promise<Viewport> {
  return { themeColor: '#f00' }
}

Structured Data (JSON-LD)

Pass raw JSON-LD as a <script> tag via metadata.other or a Script component inside the page:

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: Props) {
  const { slug } = await params
  const post = await fetchPost(slug)

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    datePublished: post.publishedAt,
    author: { '@type': 'Person', name: post.author },
  }

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>{post.body}</article>
    </>
  )
}

sitemap.ts — Auto-generate Sitemap

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await fetchAllPosts()

  return [
    { url: 'https://example.com', lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
    { url: 'https://example.com/about', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
    ...posts.map(post => ({
      url: `https://example.com/blog/${post.slug}`,
      lastModified: new Date(post.updatedAt),
      changeFrequency: 'weekly' as const,
      priority: 0.6,
    })),
  ]
}

Generates /sitemap.xml automatically.

Multiple sitemaps:

// app/sitemap/[id]/route.ts
export async function generateSitemaps() {
  return [{ id: 0 }, { id: 1 }]  // /sitemap/0.xml, /sitemap/1.xml
}

robots.ts — Generate robots.txt

// app/robots.ts
import type { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/admin/', '/api/', '/dashboard/'],
      },
      {
        userAgent: 'Googlebot',
        allow: '/',
      },
    ],
    sitemap: 'https://example.com/sitemap.xml',
  }
}

opengraph-image.tsx / twitter-image.tsx — Dynamic OG Images

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

export const runtime = 'edge'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function OGImage({
  params,
}: {
  params: { slug: string }
}) {
  const post = await fetchPost(params.slug)

  return new ImageResponse(
    <div
      style={{
        background: '#000',
        color: '#fff',
        width: '100%',
        height: '100%',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: 48,
        fontWeight: 'bold',
        padding: 64,
      }}
    >
      {post.title}
    </div>,
    { ...size }
  )
}

icon.tsx / apple-icon.tsx — Dynamic Favicons

// app/icon.tsx
import { ImageResponse } from 'next/og'

export const size = { width: 32, height: 32 }
export const contentType = 'image/png'

export default function Icon() {
  return new ImageResponse(
    <div style={{ background: '#000', width: '100%', height: '100%', borderRadius: '50%' }} />,
    { ...size }
  )
}

Metadata Inheritance and Merging

  • Layouts define defaults; pages override.
  • Object fields (like openGraph) are shallow-merged — a page must re-declare all OG fields it wants, not just the ones it changes.
  • To inherit parent OG and add one field, use generateMetadata with parent metadata:
export async function generateMetadata(
  { params }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const parentMeta = await parent
  const previousImages = parentMeta.openGraph?.images ?? []

  return {
    openGraph: {
      images: ['/new-image.png', ...previousImages],
    },
  }
}