Dynamic routes, params and metadata

Await the params promise, prerender known slugs, generate per-page metadata and Open Graph images, and know what makes a route dynamic.

params, searchParams and static params

// app/guides/[slug]/page.tsx
import { notFound } from 'next/navigation'

type Props = {
  params: Promise<{ slug: string }>
  searchParams: Promise<Record<string, string | string[] | undefined>>
}

export default async function GuidePage({ params, searchParams }: Props) {
  const { slug } = await params
  const { page } = await searchParams
  const guide = await getGuide(slug)
  if (!guide) notFound()
  return <article>{guide.title} (page {page ?? 1})</article>
}

export async function generateStaticParams() {
  const guides = await listGuides()
  return guides.map(g => ({ slug: g.slug }))   // prerender these at build time
}
  • From Next.js 15, params and searchParams are promises. Await them in a server component, or read them with React's use() hook in a client component.
  • The folder name and the property you read must match: [slug] gives { slug }. A mismatch type-checks as undefined and fails at runtime.
  • generateStaticParams prerenders the combinations you return. Routes not listed still work — they render on demand.
  • Return only path segments from generateStaticParams, not the whole record; the extra fields are ignored.
  • notFound() renders the nearest not-found file with a 404 status. Rendering an empty component instead returns 200 and gets indexed.

generateMetadata and Open Graph images

import type { Metadata } from 'next'

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const guide = await getGuide(slug)          // same call the page makes; memoised per request
  if (!guide) return { title: 'Not found' }

  return {
    title: guide.title,
    description: guide.summary,
    alternates: { canonical: '/guides/' + slug },
    openGraph: {
      title: guide.title,
      images: [{ url: '/guides/' + slug + '/opengraph-image', width: 1200, height: 630 }]
    }
  }
}

export const revalidate = 3600     // route segment config
  • A static metadata object is fine for fixed pages. generateMetadata is the async version and runs on the server, so it can query your data.
  • Within one request, the same cached fetch is shared between the page and the metadata function, so calling the loader twice costs one query.
  • The opengraph-image file convention generates a share card per route without a separate image service.
  • Set a title template once in the root layout so every page inherits a consistent suffix, then let each page supply only its own title.
  • Keep titles under roughly 60 characters and descriptions near 150; longer text is truncated in results and in preview cards.
⚠️
A slow query inside generateMetadata delays the response before any HTML is sent, because metadata resolves before the page streams. Share one cached loader between the metadata function and the page instead of querying twice.

Static or dynamic rendering

// force per-request rendering for this segment
export const dynamic = 'force-dynamic'

// or pin it to static and cache until you revalidate
export const dynamic = 'force-static'
export const revalidate = false

// reading a request value opts the route into dynamic rendering automatically
import { cookies } from 'next/headers'

export default async function Dashboard() {
  const session = (await cookies()).get('session')
  return <p>{session ? session.value : 'anonymous'}</p>
}
  • A route is static when nothing in its render path reads request data. That is the state you want for content: cacheable, cheap and fast at the edge.
  • Reading cookies or headers, or using an uncached search parameter, makes the route dynamic for everything rendered below that point.
  • useSearchParams in a client component also pushes the client tree to render in the browser; keep search-parameter work in the server component where possible.
  • A page that must be per-user cannot be cached at the CDN edge, so keep personalised content in a small region rather than making the whole page dynamic.
  • The build output labels each route static or dynamic. That report is how you confirm a change did not turn a cached page into a per-request one.

FAQ

Why is params a promise?
It allows the render to start before every segment is resolved, and it keeps the same component signature whether the value was known at build time or arrives with the request. Await it, or read it with use() in a client component.
Why is my page dynamic when I did not ask for it?
Something in the render path reads a request value: cookies, headers, a search parameter, or an uncached fetch. Find that call — it is what forces per-request rendering.

Caching, revalidation and Cache Components Routing, links and navigation

Last refreshed 2026-09-18.