Dynamic & Catch-all Segments in Depth
Master multi-param routes, optional catch-alls for docs sites, and the async params contract in Next 15.
The basics of [id] are easy; the depth is in combining segments and knowing exactly what shape params arrives in.
Multiple dynamic segments
A route can have several dynamic parts. app/shop/[category]/[product]/page.tsx gives you { category, product }. Each is a plain string.
Catch-all vs optional catch-all
[...slug]— matches one or more segments; the base path (/docs) does not match and 404s.[[...slug]]— the optional form; the base path matches too, and thereslugis simplyundefined.
That difference is why docs and wiki systems almost always use the optional form: one [[...slug]] route renders the index and every nested page.
The Next 15 async contract
params and searchParams are Promises now. You await them. This is not cosmetic — it lets Next start rendering the static shell before per-request params are resolved, which is what makes Partial Prerendering possible. Type searchParams honestly: every value is string | string[] | undefined, because ?tag=a&tag=b yields an array.
Example
// app/shop/[category]/[[...filters]]/page.tsx
// Matches /shop/shoes, /shop/shoes/red, /shop/shoes/red/size-10
type Params = { category: string; filters?: string[] };
type Search = { [key: string]: string | string[] | undefined };
export default async function Products({
params,
searchParams,
}: {
params: Promise<Params>;
searchParams: Promise<Search>;
}) {
const { category, filters = [] } = await params;
const { sort } = await searchParams;
const products = await queryProducts({ category, filters, sort });
return (
<main>
<h1>{category}</h1>
<p>Filters: {filters.join(' / ') || 'none'}</p>
<ProductGrid items={products} />
</main>
);
}When to use it
- A docs site uses [[...slug]] so both /docs and /docs/getting-started/install resolve to the same page component.
- An API route uses multiple dynamic params like [team]/[project] to scope data access in a team workspace app.
- A developer awaits the params Promise in Next.js 15 rather than destructuring synchronously to avoid runtime warnings.
More examples
Multi-param route
Multiple dynamic segments nest naturally; awaiting params gives all captured values as typed properties.
// app/[org]/[repo]/page.tsx
type Props = { params: Promise<{ org: string; repo: string }> };
export default async function RepoPage({ params }: Props) {
const { org, repo } = await params;
return <h1>{org}/{repo}</h1>;
}Optional catch-all for docs
Optional catch-all handles /docs (undefined slug) and /docs/a/b (array slug) in one component.
// app/docs/[[...slug]]/page.tsx
type Props = { params: Promise<{ slug?: string[] }> };
export default async function DocsPage({ params }: Props) {
const { slug } = await params;
const section = slug ? slug.join('/') : 'index';
const content = await getDocContent(section);
return <article dangerouslySetInnerHTML={{ __html: content }} />;
}generateStaticParams for catch-all
Returning an entry with undefined slug includes /docs itself in the static generation alongside all child paths.
// app/docs/[[...slug]]/page.tsx
export async function generateStaticParams() {
const pages = await getAllDocPages();
return [
{ slug: undefined }, // /docs
...pages.map(p => ({ slug: p.path.split('/') })),
];
}
Discussion