
# Routing & Paths

A page's `path` is the URL it answers. It is matched literally unless it contains a `:param` or `*` segment, in which case it becomes a pattern.

## Path Forms

| Form      | Example             | Matches                                                                   |
| --------- | ------------------- | ------------------------------------------------------------------------- |
| Root      | `/`                 | The home page.                                                            |
| Static    | `/about`            | That exact path, and nothing else.                                        |
| Nested    | `/settings/billing` | Any number of static segments.                                            |
| Dynamic   | `/blog/:slug`       | One segment, captured as `slug`.                                          |
| Catch-all | `/docs/:rest*`      | The remainder of the path, captured as `rest` — must be the last segment. |
| Wildcard  | `/files/*`          | Anything below `/files/`, captured under no name.                         |

A path must match `^/[a-z0-9-_/:*]*$`: it starts with `/`, and **only lowercase letters** are allowed. `/users/:userId` is rejected — write `/users/:userid`, or better, `/users/:id`.

:::callout
**A path with no `:` and no `*` is compared as an exact string.** No prefix matching, no trailing-slash tolerance. `/about` does not answer `/about/`.
:::

## Dynamic Segments

A `:param` segment captures exactly one path segment — it never spans a `/`. `/blog/:slug` matches `/blog/hello` but not `/blog/2026/hello`; use `/blog/:slug*` for the latter.

Dynamic routes are usually paired with a `collection` block, so one page definition generates a route per record:

```yaml
pages:
  - name: Blog Post
    path: /blog/:slug
    collection: { table: posts, slugField: slug }
    components:
      - { type: text, tag: h1, content: '$record.title' }
```

See [Collection & Markdown Pages](/en/docs/pages-collections) for the `collection` contract.

## Record Detail Routes

To serve one record at a URL without generating a route per record, pair a dynamic segment with a `single`-mode data source. `param` names the path segment to read the record id from:

```yaml
pages:
  - name: Task
    path: /tasks/:id
    dataSource:
      table: tasks
      mode: single
      param: id
    components:
      - { type: text, tag: h1, content: '$record.title' }
      - { type: text, content: '$record.description' }
```

A request whose parameter matches no record returns a `404`.

:::callout
**`$id` is not a path syntax.** `$` is not a legal character in a `path`, so `/tasks/$id` fails validation. The dynamic-segment marker is `:` everywhere; `$record.*` is a _content_ reference, not a route one.
:::

## The Language Segment

Sovrium does not prefix your paths for you. When `app.languages` is configured, the runtime reads the **first path segment**, and if it matches a configured language code it becomes the active language for `$t:` translation lookups.

Localized routes are therefore authored explicitly — one page per language, each carrying its own segment:

```yaml
pages:
  - { name: home-en, path: /en, components: [{ type: hero, content: '$t:hero.title' }] }
  - { name: home-fr, path: /fr, components: [{ type: hero, content: '$t:hero.title' }] }
```

## Resolution Order

An incoming request is answered by the first of these that matches:

1. A real file in the public directory.
2. A [redirect rule](/en/docs/redirects).
3. A page path.
4. The 404 catch-all.

## Related Pages

- [Pages Overview](/en/docs/pages-overview) — the full page property table.
- [Collection & Markdown Pages](/en/docs/pages-collections) — one route per record.
- [Data Binding](/en/docs/pages-data-binding) — `mode: single` and `param`.
- [Redirects](/en/docs/redirects) — retiring a path without breaking links.
- [Languages](/en/docs/languages) — configuring language codes and `$t:` keys.
