URL Redirects
Restructuring a site retires URLs. Without a redirect the only possible answer at a retired path is a 404, and every indexed link, bookmark and backlink pointing there breaks.
The redirects array declares those retired paths and where each one now lives:
redirects:
- from: /products/platform
to: /
- from: /products/partner
to: /partner
- from: /login
to: /_admin/login
status: 302Rule Properties
| Property | Description |
|---|---|
from |
Root-relative path to redirect away from. Must start with / and contain no whitespace, ? or #. |
to |
Where to send the visitor — a root-relative path, or an absolute http(s):// URL to hand off to another origin. |
status |
301 (default), 302, 307 or 308. |
Query strings are not part of the match key. An incoming query string is preserved and carried onto the target, so there is no point encoding one in from. If to already carries its own query string, the incoming one is appended to it with &.
Choosing a status
| Status | Meaning | Use it when |
|---|---|---|
301 |
Moved Permanently — transfers link equity | The URL is retired for good. This is the default and the norm. |
302 |
Found — temporary, may rewrite the request method | The move is temporary and the method does not matter. |
307 |
Temporary Redirect — temporary, preserves the method | A temporary move that must keep a POST a POST. |
308 |
Permanent Redirect — permanent, preserves the method | A permanent move that must keep the request method. |
Language Handling
A from is matched the way page paths are authored. A bare rule matches the plain path and every language-prefixed variant, and a path to inherits the language of the request — so one rule serves every locale and a French visitor lands on the French replacement, never the English one:
redirects:
- from: /pricing
to: /plansThat single rule answers /pricing, /en/pricing and /fr/pricing, sending each to /plans, /en/plans and /fr/plans respectively.
To target one locale only, write the language segment into from. A rule whose path already begins with a configured language code is matched literally:
redirects:
- from: /fr/tarifs
to: /fr/plansAn absolute to is used verbatim, with no language handling, since it leaves the app entirely.
Do not hand-write the unprefixed-path rule
A bare /docs that 404s while /en/docs serves is the one case that makes a redirect rule look obvious and destructive. The engine already handles it — see Unprefixed URLs — and both hand-written forms are now rejected at decode time, because from is matched locale-agnostically and therefore claims the live localized URLs too:
{ from: '/docs', to: '/en/docs' }
/docs → 301 /en/docs the case you wanted
/en/docs → 301 /en/en/docs the live English docs, now a 404
/fr/docs → 301 /fr/en/docs the live French docs, now a 404Reaching for localizeTarget: false fixes the double prefix and creates something worse — the rule now sends the live URL back to itself:
{ from: '/docs', to: '/en/docs', localizeTarget: false }
/docs → 301 /en/docs the case you wanted
/en/docs → 301 /en/docs infinite browser redirect loop
/fr/docs → 301 /en/docs the French docs, unreachableBoth used to pass sovrium validate and brick the docs zone at runtime. They now fail it — see Validation.
Precedence
Redirects are evaluated after static assets and before pages:
- A real file in the public directory — a redirect rule can never hijack a served asset.
- Redirect rules.
- Page resolution.
- The 404 catch-all.
A rule therefore always beats a page. That cuts both ways: a from matching a path you still serve makes that page unreachable.
Your rules are also matched before URL canonicalization, so from is compared against the path exactly as the browser sent it — trailing slash included.
Redirects resolve in exactly one hop. A matched request emits one redirect and the target is never re-matched against the table, so Location is always literally what you authored. Chains do not collapse — if /a should end at /c, write /a → /c, not /a → /b → /c.
Validation
Whole-table rules are enforced when the config is decoded, so a broken table fails sovrium validate instead of shipping. Every one of them exists because the server resolves a single hop and therefore cannot loop — but the browser follows each new rule in turn, and never settles.
| Rejected shape | Example | Why |
|---|---|---|
Duplicate from |
two rules for /pricing |
Each path may declare at most one rule. |
| Self-redirect | { from: '/a', to: '/a' } |
A rule pointing at its own from loops forever. |
| Cycle | /a → /b with /b → /a |
The browser follows each rule in turn and never arrives. |
| Double locale prefix | { from: '/docs', to: '/en/docs' } |
from is locale-agnostic, so the live /en/docs matches it too and the target is prefixed again — /en/en/docs, a 404. |
| Opt-out self-loop | { from: '/docs', to: '/en/docs', localizeTarget: false } |
from still matches every locale variant, so /en/docs is sent verbatim back to /en/docs — an infinite loop. |
| Trailing-slash self-redirect | { from: '/x', to: '/x/' } |
/x/ is normalized back to /x, which re-enters this rule. Both the emitted and the canonical form are compared. |
The last three are locale- and slash-aware and are new. { from: '/x/', to: '/x' } stays valid — it emits /x, which matches nothing, so it terminates. { from: '/en', to: '/en/' } is now rejected: it only duplicates the engine's own /en → /en/ redirect.
The opt-out check is deliberately narrow. { from: '/login', to: '/_admin/login', localizeTarget: false } is the reason localizeTarget exists and keeps validating; { from: '/legacy', to: '/en/docs', localizeTarget: false } is odd but does not loop, and is not rejected either.
Protocol-relative targets (//example.com) are also rejected. A browser resolves them as absolute cross-origin URLs, which would turn the redirect table into an open-redirect primitive; cross-origin hand-offs must spell out https:// so the intent is visible in review.
Collision with a dynamic route is not caught for you. Validation compares from against static page paths only. If your app serves a dynamic route such as /blog/:slug, a rule for /blog/my-post will silently shadow that article — no boot error, no validation failure. Check retired slugs against the content that still exists.
Related Pages
- App Metadata — the other top-level identity properties.
- Pages Overview — the page resolution that redirects run ahead of.
- Languages — the configured language codes that drive prefix matching.
- SEO & Metadata — canonical URLs, which a 301 should agree with.
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.