honour aliases as permanent redirects

An alias is a promise that an old URL keeps working, so it answers 301 to the
canonical one rather than serving the same content twice (ADR-0008). Frontmatter
takes a scalar or a list and tolerates surrounding slashes, because authors write
both.

Ambiguity is dropped, not resolved: an alias naming a real bundle, or claimed by
two bundles, is logged and ignored so the real bundle keeps its URL. Aliases
compose with language prefixes for free, since the resolver splits the language
before the key is looked up.

The redirect still fires only for an alias that exists, so a nonexistent path
cannot be probed by 301 — the property prompt 3 established.

Evidence: /pages/bio/ and /about/ both 301 to /pages/about/, /bn/pages/bio/ 301s
to /bn/pages/about/, and /pages/nothing/ is 404.
This commit is contained in:
Claude Opus 5
2026-07-30 01:45:59 +06:00
committed by bdeshi
parent 275f67dc52
commit c3fc89a913
6 changed files with 170 additions and 4 deletions
+6
View File
@@ -36,6 +36,12 @@ func serve(w http.ResponseWriter, req *http.Request, site *content.Site, r *rend
return
}
if !found {
// An alias is a promise that an old URL keeps working, so it answers a permanent redirect to the
// canonical one — and only for an alias that exists, so nothing can be probed by 301.
if canonical, isAlias := site.Alias(res.key); isAlias {
http.Redirect(w, req, content.URL(canonical, res.lang), http.StatusMovedPermanently)
return
}
http.NotFound(w, req)
return
}
+44
View File
@@ -161,3 +161,47 @@ func TestUnknownLanguagePrefixIsNotALanguage(t *testing.T) {
t.Errorf("got %d, want 404: fr is not a language this site has", rec.Code)
}
}
func aliasHandler(t *testing.T) http.Handler {
t.Helper()
fsys := fstest.MapFS{
"content/posts/new-name.md": {Data: []byte("---\ntitle: New\naliases: [posts/old-name]\n---\nMoved here.\n")},
"content/posts/new-name.bn.md": {Data: []byte("---\ntitle: নতুন\n---\nএখানে।\n")},
}
bundles, err := content.Scan(fsys)
if err != nil {
t.Fatal(err)
}
r, err := render.New()
if err != nil {
t.Fatal(err)
}
return Handler(content.NewSite(bundles), r)
}
func TestAliasRedirectsToCanonical(t *testing.T) {
h := aliasHandler(t)
for path, want := range map[string]string{
"/posts/old-name/": "/posts/new-name/",
"/bn/posts/old-name/": "/bn/posts/new-name/",
} {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
if rec.Code != http.StatusMovedPermanently {
t.Errorf("GET %s = %d, want 301", path, rec.Code)
continue
}
if loc := rec.Header().Get("Location"); loc != want {
t.Errorf("GET %s → %q, want %q", path, loc, want)
}
}
}
func TestUnknownPathIsStill404NotAnAliasProbe(t *testing.T) {
h := aliasHandler(t)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/never-existed/", nil))
if rec.Code != http.StatusNotFound {
t.Errorf("got %d, want 404", rec.Code)
}
}