Skip to content

Declarative redirect map: retire an indexed URL with a real 301 #20

Description

@cjimti

Problem

asws has three ways to answer a request that matches no file, and none of them can retire a single indexed URL correctly.

Today NoRoute offers:

  • SPA_FALLBACK=true — serve index.html with 200 for every unmatched path
  • NOT_FOUND_REDIRECT=true — send every unmatched path to one NOT_FOUND_REDIRECT_PATH, with 307
  • NOT_FOUND_FILE — serve a custom body with 404

A prerendered static site that renames a page has a narrower need than any of those. One specific old path should answer 301 to one specific new path. Everything else should keep 404ing.

Each existing mode fails that case in its own way:

  • SPA fallback turns every typo and every dead link into a 200. Search engines call this a soft 404 and it is worse than the problem it solves: the site loses the ability to tell a crawler that anything is genuinely gone.
  • Not-found redirect is all-or-nothing and points everything at one destination, so it cannot express "this page moved there."
  • Custom 404 file is honest but drops the link equity. An indexed URL returning 404 loses whatever ranking signal accumulated against it, and a client-side router redirect after the fact does not recover it, because the status code is what the crawler acts on.

The workaround people reach for is to put a reverse proxy in front of asws purely to hold a handful of redirect rules, which is a lot of moving parts for what is a static lookup table.

Proposal

A declarative redirect map, consulted in NoRoute before the SPA fallback and the 404 handling.

REDIRECTS_FILE="./www/_redirects"   # default; disabled when the file is absent

Plain-text format, one rule per line, matching the convention Netlify and Cloudflare Pages already established so existing build tooling can emit it unchanged:

/old/path   /new/path   301
/retired    /           308
/legacy     /current           # status omitted, defaults to 301

Behaviour:

  • Exact path match is enough for the motivating case and should land first. Prefix and splat matching are a reasonable follow-up, but a v1 that only does exact matches already solves this.
  • Status code per rule, defaulting to 301. 302, 307, and 308 should all be expressible; a permanent rename wants 301 or 308, and a temporary one wants 302 or 307.
  • Query strings preserved and appended to the target, so /old?utm_source=x reaches /new?utm_source=x.
  • Loaded once at startup and held in a map. These files are small and static; watching the file for changes is not worth the complexity.
  • Malformed lines logged and skipped rather than fatal, so one bad line does not take the server down. Blank lines and # comments ignored.
  • Precedence: a real file on disk always wins, since NoRoute only fires when nothing matched. Within NoRoute, redirect map first, then SPA_FALLBACK, then NOT_FOUND_REDIRECT, then NOT_FOUND_FILE. That ordering lets a site combine an honest 404 page with a handful of real redirects, which is the combination that is impossible today.

Why this belongs in asws

The current feature set already implies it. asws emits a 301 today for trailing-slash normalisation on static paths, so permanent redirects are established behaviour rather than a new concern. What is missing is a way to declare them. A static server that prerenders routes will inevitably rename some of them, and the redirect map is the piece that makes a rename non-destructive.

Acceptance criteria

  • A path listed in the redirects file responds with the configured status and a Location header pointing at the target
  • A path not listed keeps its current behaviour exactly, including the 404 status from NOT_FOUND_FILE
  • Query strings survive the redirect
  • An absent redirects file leaves all current behaviour unchanged, so this is additive and existing deployments need no config change
  • A malformed line is logged and skipped, and the remaining rules still load
  • README documents the file format, the default path, and the precedence order against the existing modes

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions