You create a dynamic route by wrapping a file or folder name in square brackets. The file pages/posts/[id].js matches /posts/1, /posts/abc and any other single segment, and the matched value becomes a route parameter.
pages/
posts/
[id].js → /posts/:id
[...slug].js → /posts/* (catch-all)
// pages/posts/[id].js
import { useRouter } from 'next/router';
function Post() {
const router = useRouter();
const { id } = router.query;
return <h1>Post: {id}</h1>;
}
export default Post;
In the Pages Router you read the parameter from router.query. When the page is prerendered without server-side rendering, router.query starts as an empty object, so id is undefined until the router is ready. The [...slug].js form is a catch-all segment, which matches several segments and passes them as an array.
In the App Router, the brackets go on the folder name, as in app/posts/[id]/page.js. The page receives params as a promise, so a Server Component awaits it and a Client Component can use the useParams hook.
This answer doesn't lend itself to a diagram - it reads best . No credits were charged.
Why there's no diagram: “”
The interactive diagram is below the answer - jump to diagram ↓ · Below it, the related concept . Jump to it ↓
The diagram below the answer is the concept . Jump to it ↓