Files
khosra/internal/web/resolve.go
T
bdeshi 80057aa183 serve language variants under a prefix
The default locale stays at the root; every other language is the same key under
/{lang}/ (ADR-0009). /en/… is never live and redirects to the root form so the URL
space cannot fork. Lookup now takes a language and reports which one it served,
following requested → default → any rather than 404ing when a translation is
missing.

That is the second routing case, so the resolver is extracted to resolve.go and
the mux keeps one entry: URL shape is the resolver's business. A leading segment
counts as a language only when some bundle is written in it, so an unknown prefix
is a 404 rather than a stripped path — and a section may not be named after a
language in use, now recorded in content-model.md.

Because the served variant can differ from the URL requested, Page gained
.Canonical (the variant actually served) and .Alternates for hreflang. A theme
must never build a path, so both come from the engine.

Evidence: /bn/pages/about/ serves the Bengali body with lang="bn" and canonical
/bn/pages/about/; /bn/posts/hello-world/ falls back to English with canonical
/posts/hello-world/; /en/pages/about/ 301s to /pages/about/; /fr/… is 404.
2026-08-01 02:23:34 +06:00

56 lines
1.8 KiB
Go

package web
import (
"strings"
"khosra/internal/content"
)
// resolution is what a request path means: which bundle key, in which language, or where to send the
// client instead.
type resolution struct {
key string
lang string
// redirect is the canonical path when the request named a non-canonical one. Non-empty means answer
// with a permanent redirect and nothing else.
redirect string
}
// resolve maps a request path to a bundle key and language.
//
// Two cases, which is why this is a resolver rather than an inline branch: the default locale lives at
// the root of the URL space, every other language under a prefix on the same key (ADR-0009). `/en/…` is
// never a live URL — it redirects to the root form so the space cannot fork. The canonical form ends in
// a slash (ADR-0008).
//
// A language prefix wins over a section of the same name, so a site with Bengali content cannot also
// have a section called `bn` (content-model.md).
func resolve(path string, site *content.Site) (resolution, bool) {
if path == "" || path[0] != '/' {
return resolution{}, false
}
trimmed := strings.Trim(path, "/")
if trimmed == "" {
return resolution{}, false
}
lang := content.DefaultLang
key := content.Normalise(trimmed)
if head, rest, found := strings.Cut(key, "/"); found && head != "" {
switch {
case head == content.DefaultLang:
// /en/… is a second spelling of the root form; send the client to the real one.
return resolution{redirect: content.URL(rest, content.DefaultLang)}, true
case site.HasLang(head):
lang, key = head, rest
}
} else if key == content.DefaultLang {
return resolution{redirect: "/"}, true
}
if !strings.HasSuffix(path, "/") {
return resolution{key: key, lang: lang, redirect: content.URL(key, lang)}, true
}
return resolution{key: key, lang: lang}, true
}