Clean URLs for static Koder sites — locale negotiation + extensionless, no trailing slash
1. Problem
The static multi-page Koder sites (the KDS at kds.koder.dev/koder.design, the area/product landings, meta.koder.dev, doc portals) carry the locale in the path and a trailing slash:
https://koder.design/pt-BR/style/palette/The owner wants the cleaner form:
https://koder.design/style/palette— no /pt-BR/ prefix, no trailing /. The bare root koder.design should serve content directly (today it is a JSmeta-refresh redirect page to `pt-BR/ or
en-US`).
2. Scope (and explicit non-scope)
In scope: the static, multi-page sites generated by tooling and served by Koder Jet — KDS (tools/design-gen), landings, meta.koder.dev, doc portals. They have one rendered file per (locale × page) under /<locale>/<path>/index.html.
Out of scope: product web apps (<produto>.koder.dev). Per specs/landing-pages/products.kmd §Padrão de URL those already root-serve a single bundle that decides what to render client-side (GitHub/Notion pattern) — their URLs are already clean and locale is a client concern. This RFC does not touch them.
3. Constraints
- Bilingual is mandatory (owner directive, alert 2026-06-14): en-US and
pt-BR (and es-ES) must always be reachable and the language toggle must stay. Dropping the locale from the path must NOT drop the capability — the explicit per-locale URLs keep existing.
- i18n contract R1/R2 already specify locale source =
Accept-Language(web,server-rendered) + persisted
koder_localecookie. This RFC is the consumer of that contract for static sites; no new locale-source mechanism is invented. - SEO must not regress. A single bare URL that serves different content per
visitor is invisible/ambiguous to crawlers unless
hreflang+ canonical are emitted correctly.
4. Decision (proposed)
R1 — Locale out of the path by default, via Jet negotiation
Koder Jet serves the negotiated-locale content at the bare path. Resolution order (owner-ratified 2026-06-14: *the first display, before any Koder login, is the DEVICE locale*):
GET /style/palette (no locale, no slash)
1. authenticated (Koder ID session)? → the account's saved locale preference
2. else koder_locale cookie set? → that locale (a prior explicit toggle on this device)
3. else Accept-Language → best q-weighted match vs the site's locales
← DEVICE LOCALE = the default first display (i18n contract R1)
4. else → x-default = en-US (technical fallback ONLY: an
unsupported device language, or a crawler with no A-L)
Response carries: Vary: Accept-Language, CookieSo an anonymous first visit shows the device's own language — en-US is never forced when the device asks for a language we publish. The explicit /<locale>/style/palette URLs remain (canonical, shareable, crawlable). The language toggle sets koder_locale and navigates to the clean URL; an explicit /pt-BR/... link pins that locale (also sets the cookie). A logged-in user's Koder ID locale preference wins over device/cookie (ties into RFC-018 SSO, live on prod).
R2 — Extensionless, no trailing slash; one canonical form
/style/palette ← canonical (served from .../style/palette/index.html)
/style/palette/ → 301 → /style/palette
/style/palette.html → 301 → /style/palette
/<locale>/style/palette ← per-locale canonical (same no-slash rule)Root / serves the negotiated home directly (no redirect page).
R3 — SEO: canonical + hreflang (Google language-negotiation pattern)
Each page emits:
<link rel="canonical">= the per-locale clean URL (one canonical perlanguage version — the indexable unit).
<link rel="alternate" hreflang="pt-BR|en-US|es-ES">= the per-locale clean URLs,plus
hreflang="x-default"= the bare negotiated URL.- Sitemap lists the per-locale URLs; the broken-links gate (
policies/environments.kmd)follows the canonical no-slash form.
This is the configuration Google documents for Accept-Language negotiation: the bare URL is the x-default negotiated entry; the per-locale URLs are what gets indexed.
5. Mechanism
- Koder Jet (
policies/web-server.kmd) gains a per-site *lean-URL + localenegotiation*mode (vhost flag): internal rewrite
/(path)→/<locale>/(path)/index.htmlwithVary, a 301 normalizer for trailing-slash.html, and the cookieAccept-Language match. No app code; pure static serving config. - KDS generator (
tools/design-gen): emit canonical+hreflang per R3, switch thein-page asset/nav links + the language toggle to the clean form, keep the per-locale files on disk (no dist layout change — Jet maps bare→
/<locale>/).
6. Pilot → rollout
- Pilot on the KDS (
kds.koder.dev+koder.design) — highest-traffic staticsite, fully owned here, easy to verify headless. Ship behind the Jet vhost flag.
- Soak + verify (headless: bare URL negotiates; toggle sticks; old
/pt-BR/...301s;hreflang present; broken-links gate green).
- Generalize the Jet flag + generator emitters to the landings +
meta.koder.dev.
7. Open questions (owner-decision)
- O1 — default first-display locale: RESOLVED (owner 2026-06-14). Before any
Koder login, the first display uses the device locale (
Accept-Language), per i18n contract R1 — see R1 step 3.x-default = en-USis only the technical fallback (unsupported device language / crawler), never forced over an available device language. - O2 — keep
/<locale>/visibly or only as canonical-under-the-hood? Proposed:keep them reachable (pinning + SEO) but the toggle/nav use the clean form, so users normally never see a locale in the bar.
- O3 — apply to
meta.koder.devand landings in the same wave or KDS-only first?Proposed: KDS-only pilot, then wave.
8. Non-goals
- No change to product web app URLs (already clean — §2).
- No new locale-source mechanism (reuses i18n contract R1/R2).
- No removal of any language; the toggle and all locales stay (owner directive).
9. Status
Draft — awaiting owner ratification of R1–R3 + the open questions (O1–O3). On ratification: implement the Jet clean-URL/negotiation mode, pilot on KDS, then wave.