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,
paramsandsearchParamsare promises. Await them in a server component, or read them with React'suse()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. generateStaticParamsprerenders 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
metadataobject is fine for fixed pages.generateMetadatais 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-imagefile 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.
useSearchParamsin 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.
Related
Caching, revalidation and Cache Components Routing, links and navigation
Last refreshed 2026-09-18.