diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf index c9c8b95..57f8c7c 100644 --- a/apps/web/nginx.conf +++ b/apps/web/nginx.conf @@ -1,3 +1,23 @@ +# The SPA shell's `lang` attribute, negotiated from the request (issue #179, +# WCAG 3.1.1). `apps/web/index.html` is a static file with a hard `lang="en"`; +# the app corrects it at runtime (#163), but a crawler or a no-JS visit of an +# SPA route — which nginx answers with index.html — would see `en` forever, +# even for German content. +# +# Only the FIRST tag of Accept-Language decides, which is what "the browser's +# preferred language" means and mirrors #163's semantics. `de-CH` counts as +# German; `en-US,de` does not, because that visitor asked for English first. +# +# Known limit, documented rather than worked around: nginx does not know +# `instance.defaultLocale` from the database, so a visitor with no (or an +# unlisted) Accept-Language gets `en` even on a German instance. For PUBLIC +# content that is not the authoritative rendering anyway — the api's server +# shell (`/api/v1/public/...`) renders those with the instance locale. +map $http_accept_language $spa_lang { + default en; + ~*^de de; +} + # SPA serving: static assets with long-lived caching, everything else # falls back to index.html (client-side routing). server { @@ -34,6 +54,17 @@ server { # zero-third-party-request guarantee (security.md) is unaffected. add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob:; connect-src 'self'; worker-src 'self'; manifest-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always; add_header X-Content-Type-Options "nosniff" always; + # The shell's language (issue #179). Only the html document is + # rewritten, and only its first match — `` is the + # first and only occurrence in index.html. Everything else this + # location serves passes through untouched. + sub_filter_types text/html; + sub_filter_once on; + sub_filter 'lang="en"' 'lang="$spa_lang"'; + # The response now depends on a request header, so shared caches must + # not serve one language's copy to the other. This location is + # `no-cache` anyway; the header states the dependency correctly. + add_header Vary "Accept-Language" always; try_files $uri /index.html; } }