The docs server exposes every resource under two parallel URL spaces, picks a visitor-facing language with a small explicit precedence chain, and rewires SFX’s chrome to match. This chapter is the behavior reference; the original design notes live in LANG_AND_SEO_BEHAVIOR.md and are still worth diffing against the code when something here looks stale.
Every page has up to two stable URLs:
/<**path>, e.g. /policies/. Always exists. Default language unless overridden by the visitor’s lang cookie./<code>/<**path>, e.g. /zh/policies/, /ja/policies/. Exists for each non-default language listed in support_lang.json.Both forms exist because they serve two different audiences. Humans get the bare URL and a cookie that sticks across pages — they don’t see /en/ cruft on what they think of as “the site.” Crawlers, social-link unfurlers, and anyone sharing a language-specific link get unambiguous per-language URLs so hreflang works and og:url doesn’t collapse three translations into one canonical. The default language deliberately has only the bare form so /policies/ and /en/policies/ never compete for the same <link rel="canonical">.
support_lang.jsonThe shape is a flat JSON array of language codes:
["en", "zh", "ja"]
The first entry is the default language, served at the bare URL with no prefix endpoint generated for it. Every subsequent entry produces one generated /<code>/<**path> endpoint at build time. build.rs reads this file, validates each non-default code against [a-z0-9_-]+ (defense against URL-injection at codegen time), and writes endpoint! blocks into $OUT_DIR/lang_endpoints.rs, which lib.rs include!s. The cargo:rerun-if-changed=programfiles/op/support_lang.json directive ensures edits to the list re-trigger codegen.
Each generated endpoint is a thin shim. For "zh" it expands to roughly:
endpoint! {
APP.url("/zh/<**path>"),
pub view_zh <HTTP> { crate::routes::lang_prefix::handle(req, "zh").await }
}
lang_prefix::handle does three things and then delegates:
LangOverride(lang) on req.params — a request-scoped marker carrying the prefix code.EffectivePath(stripped) on req.params, where strip_lang_prefix removes the leading /<code> (guarding /zh-abc against being misread as /zh).crate::routes::view::dispatch(req).await — the same dispatcher the bare /<**path> endpoint enters.The downstream handler chain (render, edit::handle, download::handle, permissions::handle) is identical for bare and prefix requests. Lang and path are the only things that differ, and both flow through the helpers below.
lang_for resolution orderlang_for(req) is the single source of truth for the visitor-facing language:
req.params.get::<LangOverride>()
.map(|o| o.0.clone())
.or_else(|| sfx::op::lang_or_none(req))
.unwrap_or_else(sfx::op::default_lang)
Order matters and prefix wins. A logged-in JA reader who clicks a /zh/... link must see Chinese — the URL is the contract. Falling back to cookie next preserves human stickiness on bare URLs; falling back to default_lang covers the cookieless first visit. The ?lang=… query that SFX still supports via lang_or_none is intentionally never used as a primary signal on docs surfaces — prefix URLs replaced it for SEO, and the cookie replaced it for sticky preference.
req_path(req) returns the prefix-stripped resource path: handlers see /policies/ whether the visitor hit /policies/ or /zh/policies/. When EffectivePath is absent (bare URL), it just returns req.path().
url_for(req, path) prepends the active /<code> prefix to an absolute path only when a LangOverride is in scope. Every absolute URL builder — breadcrumb step paths, BoardGroup section links, edit/download/permissions redirect targets — uses it so navigation stays under the chosen language.
current_prefix(req) returns "/zh" or "". prefix_breadcrumb(req, bc) walks a path_value() array and rewrites each path field through the same prefix.
canonical_url_for(req, path) combines public_origin() with url_for(req, path). Bare requests canonicalize to bare; /zh/foo/ canonicalizes to <origin>/zh/foo/, never collapsing to the bare form. When public_origin is unset it returns the empty string and the template’s SEO guard drops the entire block.
pageprop_forSFX’s own op::pageprop reads SFX’s lang() (cookie/query/default) to populate lang, nav, and foot on the page-prop bag. It has no idea our prefix endpoints exist, so a cookieless crawler on /zh/policies/ would otherwise get Chinese body content but <html lang="en"> and an English navbar. pageprop_for(req, name, description, lang) calls through to op::pageprop and then overrides pp.lang, pp.nav, and pp.foot using the locally cached static NAVBAR and static FOOTER (loaded from programfiles/op/navbar.json and programfiles/op/footer.json because SFX’s equivalents are private). Every render arm in content::render calls it.
/op/switch_lang/<code>The dedicated switcher that replaces SFX’s cookie-only /op/lang/<code>. Steps:
normalize_lang lowercases the path param and whitelists against content::support_langs(); unsupported codes fall back to default_lang.switch_source extracts the source URL: ?from=<path> query first, then Referer: (or Referrer:), else /.normalize_source_url reduces the source to a path-only string.strip_supported_lang_prefix removes any existing /<code>/ from that path.apply_target_lang re-attaches /<target>/ unless the target is the default lang.302 with Set-Cookie: lang=<target>; Path=/; HttpOnly and Cache-Control: no-store. Non-GET methods get 405.switch_langnormalize_source_url ends with:
if path.is_empty() || path.starts_with("//") {
"/".to_string()
} else {
path
}
The // check is load-bearing. Without it, ?from=//evil.com/path injects a network-path reference directly, and Referer: http://host//evil.com survives the scheme-stripper as //evil.com. Either would produce Location: //evil.com/…, which browsers parse as protocol-relative and follow off-origin — a classic open redirect. Collapsing to / shuts both vectors.
hreflang_alternates (preview)hreflang_alternates(path, current_lang) emits one entry per supported language plus x-default. Default lang gets the bare URL (<origin><path>); non-default langs get <origin>/<code><path>. Each entry carries is_alt: code != current_lang so the template can drive og:locale:alternate without duplicating og:locale. Full coverage — including the x-default rule, JSON-LD breadcrumbs, and the sitemap — lives in SEO and sitemap.
programfiles/op/support_lang.json.programfiles/op/navbar.json and programfiles/op/footer.json.build.rs re-runs, emits the new prefix endpoint, and lib.rs picks it up.name and desc dicts on existing ctx.json files so resources have a label in the new language.See Extending the server for the broader walkthrough.
ctx.json whose name or desc is a plain string instead of a LangDict serves that value verbatim in every language — Header.name.get(lang) falls through to the single string.name as a LangDict even if you only fill in the default language. Manual edits that switch to a plain string are legal but lossy.dev and admin doc pages here have multi-language name/desc headers but a single English .md body. That combination is intentional for English-only reference docs; see the admin guide for the editor flow that produces it.