Skip to main content
View as Markdown

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.

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:

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

See Collection & Markdown Pages 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:

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

A request whose parameter matches no record returns a 404.

The Language Segment

Sovrium never prefixes a path that already resolves. 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:

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

The one place a prefix is added is the 404 branch. An unprefixed path that resolves under no page, but would resolve once prefixed, is answered with a 302 to /{lang}{path} instead of a 404 — see Unprefixed URLs.

Trailing Slashes

Page paths are authored without a trailing slash (path: '/about'), and that slash-free form is canonical: it is what every internal link, hreflang alternate and sitemap entry the engine emits points at. A request that arrives with a trailing slash is normalized toward it with a 301, then resolved again.

Request Answer
/about/ 301/about
/en/docs/ 301/en/docs — the arriving locale is kept
/docs// 301/docs — repeated slashes collapse in one hop
/ 200 — the root is exempt
/en/ 200 — the bare language root is exempt
/nope/ 404, with no Location

The incoming query string is carried onto the target byte-identically, so campaign attribution on an inbound link survives the hop.

The two exemptions are not cosmetic. /en already 301s to /en/ (see Languages), so stripping /en/ back to /en would bounce the browser between the two forever. Only the bare language root is exempt — /en/docs/ is normalized like any other path.

The canonical form must resolve. A 301 is emitted only when the slash-free path actually answers — directly, or through the unprefixed-path fallback. /nope/ therefore stays a clean 404 rather than becoming a 301 into a 404, which burns the redirect, still fails, and walks a crawler into a dead end.

A page may also be authored with a trailing slash: path: '/docs/' is legal, and a request to /docs/ is then served in place rather than stripped. The config is the authority.

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.
  3. A page path — compared exactly, including a trailing slash the path was authored with.
  4. Trailing-slash normalization — a 301 to the slash-free path, when that path resolves.
  5. The unprefixed-path language fallback — a 302 to /{lang}{path}, when that path resolves.
  6. The 404 catch-all.

Steps 4 and 5 run only where step 3 declined, so every URL that resolves today keeps resolving byte-identically. They also compose: /docs/ is answered 301/docs, and the follow-up request is answered 302/en/docs.

Both are server mode only. sovrium build emits no redirects of any kind, so a statically hosted site answers /about/ and /docs with whatever its host is configured to do.

Last updated September 1, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.

Built with Sovrium