An ellipsis inside the brackets extends a dynamic segment over the rest of the path. [...slug] matches one or more segments and hands you an array; [[...slug]] does the same but also matches the bare parent route, where the parameter is simply absent.
export default async function Shop({ params }) {
const { slug } = await params
return <pre>{JSON.stringify({ slug })}</pre>
}Both demo routes print their own params, so the difference is visible directly in the served HTML:
/shop/clothes/tops/t-shirts <pre>{"slug":["clothes","tops","t-shirts"]}</pre>
/docs/app/routing <pre>{"slug":["app","routing"]}</pre>
/docs <pre>{}</pre>/docs renders with no parameter at all, while /shop — the same route without the extra brackets — is one of the 404s in the probe of Route Groups.
| Route | /shop | /shop/a | /shop/a/b |
|---|---|---|---|
| [slug] | 404 | 'a' | 404 |
| [...slug] | 404 | ['a'] | ['a', 'b'] |
| [[...slug]] | undefined | ['a'] | ['a', 'b'] |
Use the optional form whenever one component should own a whole subtree including its index — documentation sites, file browsers, CMS-driven pages. The parameter may be undefined there, so slug?.join('/') ?? '' is the normal way to rebuild the path. A catch-all also swallows everything below it, including URLs you meant to 404; validate the array and call notFound() rather than rendering an empty shell.