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.tsxapplies as a default to all nested routes;page.tsxmetadata 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
generateMetadatawith 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], }, } }