Clean URLs for static Koder sites — locale negotiation + extensionless, no trailing slash

draft

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_locale cookie. 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, Cookie

So 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 per

    language 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 + locale

    negotiation*mode (vhost flag): internal rewrite /(path)/<locale>/(path)/index.html with Vary, 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 the

    in-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

  1. Pilot on the KDS (kds.koder.dev + koder.design) — highest-traffic static

    site, fully owned here, easy to verify headless. Ship behind the Jet vhost flag.

  2. Soak + verify (headless: bare URL negotiates; toggle sticks; old /pt-BR/... 301s;

    hreflang present; broken-links gate green).

  3. 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-US is 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.dev and 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.