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.
56 lines
1.8 KiB
Go
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
|
|
}
|