Dynamic Routes

Brackets in a filename capture a piece of the URL.

Name the file [id].html and every /users/<anything> lands on it.

source
pages/
└── users/
    └── [id].html    → /users/:id

The captured value arrives on window.route.params:

source
<!-- pages/users/[id].html -->
<script>
const userId = window.route.params.id
</script>

<p>User ID: {userId}</p>

Visit /users/123 and userId is the string "123".

Params are always strings - wrap one in Number(...) if you need a number.

Several in one path

Each bracketed segment becomes its own param:

pages
[slug].html/blog/:year/:month/:slug

Select a file to see what it is for

source
<!-- pages/blog/[year]/[month]/[slug].html -->
<script>
const { year, month, slug } = window.route.params
</script>

<h1>{slug}</h1>
<p>Published: {month}/{year}</p>

A named file wins

Static routes are matched before dynamic ones:

pages
me.html/users/me (matched first)
[id].html/users/:id (everything else)

Select a file to see what it is for

So /users/me loads me.html and /users/42 loads [id].html.

You never have to order anything yourself.

Linking to one

Put the real value in the href:

source
<script>
let users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
]
</script>

<ul>
  {users.map(user => (
    <li>
      <a href="/users/{user.id}">{user.name}</a>
    </li>
  ))}
</ul>
The filename names the param. [id].html gives you params.id, [slug].html gives you params.slug. Brackets keep their capitals, so [userId].html is params.userId.

What goes in the brackets

A letter or underscore, then letters, digits or underscores - the same shape as a JavaScript variable, because the name becomes params.name and a generated type.

[id], [userId] and [post_id] route. [user-id], [a.b], [123] and [$id] do not: the build warns, naming the file, and the page routes at its literal path instead.

There is no catch-all. A dynamic segment matches exactly one path segment, always, so [id].html answers /users/42 and never /users/42/edit. The Next.js and Nuxt spelling [...slug].html is not a JAF route - the build says so rather than leaving you with a 404 that looks like a config problem. Route the levels you actually have: docs/[section]/[page].html.

A param the URL cannot decode

/users/%zz is not valid percent-encoding. The route still matches and params.id holds the literal text "%zz" - the same answer a query param gives - so a hand-edited or truncated link cannot take the page down.

Dev builds warn, naming the reader and the segment.