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.
A path with no : and no * is compared as an exact string. No prefix matching — /about is not the same route as /about/. A request carrying a trailing slash is not lost, though: it is normalized away with a 301 and resolved again, so /about/ reaches /about. See Trailing Slashes.
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:
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:
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.
$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 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:
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.
Canonicalization never reveals a page you cannot read. A canonicalizing 301 or 302 fires only toward a page an anonymous visitor may open. If /vault is role-gated, /vault/ answers 404 — byte-identical to the answer for a path that was never declared. Redirecting would announce that /vault exists, which is exactly the fact its own 404 is there to hide. The cost is that the convenience does not extend to gated pages for anyone, signed in or not: the check runs in the router, which has no session.
Resolution Order
An incoming request is answered by the first of these that matches:
- A real file in the public directory.
- A redirect rule.
- A page path — compared exactly, including a trailing slash the
pathwas authored with. - Trailing-slash normalization — a
301to the slash-free path, when that path resolves. - The unprefixed-path language fallback — a
302to/{lang}{path}, when that path resolves. - 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.
Related Pages
- Pages Overview — the full page property table.
- Collection & Markdown Pages — one route per record.
- Data Binding —
mode: singleandparam. - Redirects — retiring a path without breaking links.
- Languages — configuring language codes and
$t:keys.
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.