Files
apf_portal/infra/local/Caddyfile
T
Julien Gautier 1a8e2cbb21
CI / commits (pull_request) Successful in 2m33s
CI / scan (pull_request) Successful in 2m33s
CI / check (pull_request) Successful in 2m41s
CI / a11y (pull_request) Successful in 2m18s
CI / perf (pull_request) Successful in 5m50s
feat(infra): local serve-static profile — Caddy reverse proxy for the prod build
Add a Caddy reverse proxy behind a new `--profile serve-static` so a
contributor can exercise the production build with the per-locale
routing that the on-prem reverse proxy will use (ADR-0019). Closes
the gap PR #96 surfaced: the locale switcher / route fusion / cookie
plumbing all need a prod-faithful local setup, and `nx serve-static`
falls short (no SPA fallback per locale, no smart `/` redirect).

What lands:

- `infra/local/Caddyfile` — three-block route:
    * `GET /` → smart redirect to `/{locale}/` based on
      `Accept-Language` (FR fallback, since the APF audience is
      francophone). Cookie support waits on the BFF route in
      ADR-0019.
    * `/fr/*` → serves `dist/.../browser/fr/` with SPA fallback to
      `index.html` for deep links.
    * `/en/*` → mirror.
    * Catch-all → bounces to `/fr/` so a typo can't land on the
      Caddy directory-listing footgun (same family as the perf-gate
      fix in #92).
- `infra/local/dev.compose.yml` — new `serve-static` service on the
  `serve-static` profile, bind-mounting the Caddyfile and the
  `dist/apps/portal-shell/browser/` folder (read-only). Default
  port 4200, override via `SERVE_STATIC_PORT` in `.env`.
- `infra/local/.env.example` — adds `SERVE_STATIC_PORT=4200`.
- `infra/local/dev.sh` — registers `serve-static` in
  `ALL_PROFILES` so `dev.sh down|status|logs` catches the new
  container, and `dev.sh up serve-static` works.
- `infra/README.md` — adds the new file row, the workflow in the
  "First-time setup" snippet, the cheat-sheet row, and the service
  endpoint row with the prerequisite `nx build … -c=production`.

Workflow once merged:

```
pnpm exec nx build portal-shell --configuration=production
./infra/local/dev.sh up serve-static
open http://localhost:4200/   # → /fr/ or /en/ per Accept-Language
```

Verified locally:

- GET / with Accept-Language=fr → 302 /fr/
- GET / with Accept-Language=en → 302 /en/
- GET /unknown → 302 /fr/
- GET /fr/deep/route → 200 (SPA fallback to fr/index.html)
- GET /fr/favicons/favicon.svg → 200 (per-locale asset served)
- /fr/index.html has `lang="fr"` and `<base href="/fr/">`
- /en/index.html has `lang="en"` and `<base href="/en/">`

This is local convenience only — no TLS, no auth, binds to
localhost. The on-prem reverse proxy gets its own ADR (phase 3b).
2026-05-12 00:03:59 +02:00

75 lines
2.4 KiB
Caddyfile

# Local manual-test reverse proxy for `portal-shell`'s production
# build, simulating what the on-prem reverse proxy will do (per
# ADR-0019 §"URL strategy"):
#
# - GET / → smart redirect to /{locale}/ based on
# Accept-Language (cookie support lands when
# the BFF route is in place). Falls back to
# `/fr/` since the APF audience is francophone.
# - GET /fr/<any> → serve `dist/.../browser/fr/<any>`, with SPA
# fallback to `fr/index.html` for deep links.
# - GET /en/<any> → mirror for the EN bundle.
# - everything else → redirect to `/fr/` (avoids the http-server
# directory-listing footgun we hit in the perf
# gate while the previous fix landed).
#
# Used by:
# - `./infra/local/dev.sh up serve-static` (after building the
# production bundle with `nx build portal-shell -c=production`).
# - Convenient for testing the locale switcher end-to-end, the
# accessibility-route redirect, dark-mode preference plumbing,
# etc. — anything that needs both locale folders served at their
# own prefix.
{
auto_https off
admin off
}
:4200 {
encode gzip zstd
log {
output stdout
format console
level INFO
}
# Group the locale-routing logic in an explicit `route` block so
# directive ordering follows source order, not Caddy's default
# directive priority — handle / handle_path / redir would
# otherwise interleave in surprising ways.
route {
# Smart redirect for the bare root.
@root path /
handle @root {
@prefers_en header Accept-Language en*
redir @prefers_en /en/ 302
redir * /fr/ 302
}
# French bundle. `handle_path` strips `/fr` before passing the
# request through, so `try_files` looks at `<path>` relative
# to the bundle root (`/srv/dist/fr/`). Missing files fall
# back to `index.html`, exactly like a production deep-link
# (e.g. /fr/accessibility) would do via a real proxy.
handle_path /fr/* {
root * /srv/dist/fr
try_files {path} /index.html
file_server
}
# English bundle, mirror of the above.
handle_path /en/* {
root * /srv/dist/en
try_files {path} /index.html
file_server
}
# Anything that didn't match a locale prefix falls back to FR
# home — same defaulting as the bare root above. Keeps
# typo'd URLs from landing on a 404.
redir * /fr/ 302
}
}