Skip to main content
View as Markdown

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:

app.yaml
redirects:
  - from: /products/platform
    to: /
  - from: /products/partner
    to: /partner
  - from: /login
    to: /_admin/login
    status: 302

Rule 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:

app.yaml
redirects:
  - from: /pricing
    to: /plans

That 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:

app.yaml
redirects:
  - from: /fr/tarifs
    to: /fr/plans

An 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:

code
{ 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 404

Reaching for localizeTarget: false fixes the double prefix and creates something worse — the rule now sends the live URL back to itself:

code
{ 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, unreachable

Both 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:

  1. A real file in the public directory — a redirect rule can never hijack a served asset.
  2. Redirect rules.
  3. Page resolution.
  4. 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.

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.

  • 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.

Built with Sovrium