package web import ( "strconv" "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 // page is 1 for a bundle or the first listing page, higher for /page/N/. page int // 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 } page := 1 // A trailing /page/N/ is pagination, not part of the key (ADR-0028). Page one is the bare listing // URL, so /page/1/ is a second spelling and redirects. if rest, n, isPaged := cutPage(key); isPaged { if n == 1 { return resolution{redirect: content.PageURL(rest, lang, 1)}, true } key, page = rest, n } if !strings.HasSuffix(path, "/") { return resolution{key: key, lang: lang, page: page, redirect: content.PageURL(key, lang, page)}, true } return resolution{key: key, lang: lang, page: page}, true } // cutPage strips a trailing "page/N" off a key, reporting the page number. func cutPage(key string) (rest string, page int, ok bool) { i := strings.LastIndex(key, "/") if i < 0 { return key, 1, false } n, err := strconv.Atoi(key[i+1:]) if err != nil || n < 1 { return key, 1, false } switch base := key[:i]; { case base == "page": return "", n, true case strings.HasSuffix(base, "/page"): return strings.TrimSuffix(base, "/page"), n, true } return key, 1, false }