dorfteich/apps/web
Claude Opus 5 9fbaeb559b
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m13s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 9m45s
CI / Import/export fidelity gate (pull_request) Successful in 56s
#179: negotiate the SPA shell's lang attribute in nginx
`apps/web/index.html` carries a hard `lang="en"`. The app corrects it at
runtime (#163), but nginx answers every SPA route with that same file, so a
crawler or a no-JS visit — `/public/...` on prod is exactly that — saw `en`
for German content, permanently. WCAG 3.1.1 is about the delivered document,
not the one JavaScript later fixes.

nginx-only, no backend involved: a `map` on `Accept-Language` and a
`sub_filter` in the index.html path. Only the FIRST tag decides, which is
what "the browser's preferred language" means and mirrors #163 — `de-CH`
counts as German, `en-US,de` does not.

`Vary: Accept-Language` is new. The response now genuinely depends on a
request header, and without it a shared cache could hand one language's copy
to the other. Everything else in the location is untouched: same CSP, same
`Cache-Control: no-cache`, same `nosniff`.

The known limit is documented in the config rather than worked around: nginx
cannot read `instance.defaultLocale`, so an unlisted or absent
Accept-Language yields `en` even on a German instance. For public content
that is not the authoritative rendering anyway — the api's server shell
(`/api/v1/public/...`) already renders those with the instance locale.

Verified against a real nginx 1.27 (the image the stage runs) with the
config mounted as-is: `de-DE,de;q=0.9,en;q=0.8` and `de` yield `lang="de"`;
`en-US,en;q=0.9`, `en-US,en;q=0.9,de;q=0.8`, `fr-FR,fr` and a request with no
header yield `lang="en"`; an SPA route (`/public/teich/seite`) negotiates the
same way; the gzipped response is rewritten too, and a JavaScript asset comes
through byte-identical.
2026-08-01 19:30:59 +02:00
..
e2e #304: scope the legal spec's status locator to its own form 2026-08-01 19:05:18 +02:00
public #180: dark mode — Light/Dark/System setting with token-based dark palette 2026-07-28 20:40:47 +02:00
src #304: scope the legal spec's status locator to its own form 2026-08-01 19:05:18 +02:00
Dockerfile #236: pin the Node version 2026-07-31 04:14:55 +02:00
index.html #180: dark mode — Light/Dark/System setting with token-based dark palette 2026-07-28 20:40:47 +02:00
nginx.conf #179: negotiate the SPA shell's lang attribute in nginx 2026-08-01 19:30:59 +02:00
package.json #171: A11y-Tooling — axe-Smoke-Pack in CI 2026-07-21 15:24:56 +02:00
playwright.config.ts Add CD workflow: build-push, deploy Test, smoke suite, promote Int 2026-07-04 19:57:16 +02:00
tsconfig.json Add React SPA shell with routing, layout, and API status 2026-07-04 19:19:12 +02:00
vite.config.ts Add offline editing: local persistence, PWA shell, offline resolution (#38) 2026-07-08 22:08:55 +02:00
vitest.config.ts Keep Playwright specs out of Vitest collection 2026-07-05 00:28:28 +02:00