This PR contains the work done to refactor the _Localization_ package and tidy the project a little bit. To provider further details about the work: * The `Negotiate` function was moved to the _Localization_ package. * The `WebsiteRequestContext` context no longer resolves a default language at creation; it starts empty and relies on the `LocalizationMiddleware` middleware to fill it in. * Fixed the local dependency path to Packages/Localization for the **Website** package as it only resolved inside the Xcode workspace before, breaking swift build, the Makefile, and the Docker build). * Updated the `README` file to document language negotiation, the GET /health route, and the full middleware chain; doc comments across the moved/renamed types were brought back in sync with the code. Reviewed-on: rock-n-code/loud-amsterdam#11 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
Loud Website
The Loud public website service — a Hummingbird server that renders a static landing page and serves the site's static assets.
Overview
The service:
- Serves the landing page at
GET /(rendered once per supported language with Elementary and cached). - Negotiates each request's language from its
Accept-Languageheader against the languages in theWebsiteCoreString Catalog, falling back to the default (en); pages are served from the per-language cache withContent-LanguageandVary: Accept-Languageheaders. - Answers health checks at
GET /healthwith a static JSON payload. - Serves static files (CSS, JS, icons, manifest,
robots.txt) fromResources/Staticvia Hummingbird'sFileMiddleware, tagged with media-type-specificCache-Control. - Returns a custom HTML 404 page, localized like the landing page, for any request that matches neither a route nor a static file.
- Compresses responses (gzip/deflate) above a configurable size when the client advertises support.
- Stamps a hardened set of security headers on every response.
Requirements
- Swift 6.3 toolchain (
swift-tools-version:6.3). - Docker (optional) for the containerized run/deploy workflow.
Architecture
Two SwiftPM targets:
| Target | Kind | Path | Role |
|---|---|---|---|
Website |
executable | Sources/App |
Entry point: reads configuration, builds and runs the application. |
WebsiteCore |
library | Sources/Library |
Controllers, middlewares, pages, cached responses, and configuration helpers. |
WebsiteCore also depends on the local Localization package (Packages/Localization), which provides the Localize and Negotiate helpers and the LanguageList of catalog languages.
Requests pass through the middleware chain in this order (outermost first), then reach the routes:
LogRequestsMiddleware
→ SecurityHeadersMiddleware (security headers on every response)
→ ResponseCompressionMiddleware (gzip/deflate above the size threshold)
→ LocalizationMiddleware (negotiates the request's language)
→ NotFoundMiddleware (renders the localized 404 page on .notFound)
→ FileMiddleware (serves Resources/Static)
RootController (GET / → landing page)
HealthController (GET /health → health check)
Configuration
Configuration is read through swift-configuration from the following sources, highest precedence first:
- Command-line arguments (e.g.
--http-host 0.0.0.0) - Process environment variables
- A
.envfile in the working directory (optional) - Built-in defaults
Environment variable naming
A dotted config key maps to an environment variable by upper-casing, splitting camelCase, and replacing separators with _. For example http.serverName → HTTP_SERVER_NAME,
security.strictTransportSecurity → SECURITY_STRICT_TRANSPORT_SECURITY, cache.maxAge.text → CACHE_MAX_AGE_TEXT.
To disable a header or override a value, leave the variable unset to fall back to the default. A variable that is set but blank is treated as an explicit empty value, not as "use the default".
Static file caching
| Config key | Environment variable | Default | Description |
|---|---|---|---|
cache.maxAge.text |
CACHE_MAX_AGE_TEXT |
3600 (1 hour) |
max-age for text assets (CSS, JS, plain text); also marked must-revalidate. |
cache.maxAge.image |
CACHE_MAX_AGE_IMAGE |
604800 (1 week) |
max-age for images (ICO, PNG, SVG). |
cache.maxAge.default |
CACHE_MAX_AGE_DEFAULT |
86400 (1 day) |
max-age for everything else (e.g. the web manifest). |
Response compression
| Config key | Environment variable | Default | Description |
|---|---|---|---|
compression.minimumResponseSize |
COMPRESSION_MINIMUM_RESPONSE_SIZE |
1024 |
Minimum response body size, in bytes, before compression is applied. |
HTTP server
| Config key | Environment variable | Default | Description |
|---|---|---|---|
http.host |
HTTP_HOST |
none | Host the server binds to. Supplied via the --http-host CLI flag (the Docker image passes 0.0.0.0). |
http.port |
HTTP_PORT |
none | Port the server listens on. Supplied via the --http-port CLI flag (the Docker image passes 8080). |
http.serverName |
HTTP_SERVER_NAME |
LoudWebsite |
Server name and logger label. |
Logging
| Config key | Environment variable | Default | Description |
|---|---|---|---|
log.level |
LOG_LEVEL |
info |
Minimum log level. |
Paths
| Config key | Environment variable | Default | Description |
|---|---|---|---|
path.staticFiles |
PATH_STATIC_FILES |
Resources/Static |
Directory, relative to the working directory, that static files are served from. |
Security headers
| Config key | Environment variable | Default |
|---|---|---|
security.contentSecurityPolicy |
SECURITY_CONTENT_SECURITY_POLICY |
default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' |
security.contentTypeOptions |
SECURITY_CONTENT_TYPE_OPTIONS |
nosniff |
security.frameOptions |
SECURITY_FRAME_OPTIONS |
DENY |
security.referrerPolicy |
SECURITY_REFERRER_POLICY |
strict-origin-when-cross-origin |
security.permissionsPolicy |
SECURITY_PERMISSIONS_POLICY |
accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=() |
security.strictTransportSecurity |
SECURITY_STRICT_TRANSPORT_SECURITY |
none (omitted) |
Strict-Transport-Security has no default and is omitted unless explicitly configured: it only takes effect over HTTPS (browsers ignore it on plain HTTP) and is "sticky" in browsers, so it must stay off in local HTTP development. It is enabled for production in docker-compose.yml, where it only has an effect once traffic is served over HTTPS behind a TLS-terminating proxy.
Running locally
Directly with Swift:
swift run Website # binds to Hummingbird's default 127.0.0.1:8080
swift run Website --http-host 0.0.0.0 --http-port 9000 --log-level debug
Or via the Makefile / Docker (uses docker-compose.override.yml, which builds from source and sets LOG_LEVEL=debug):
make pkg-build # swift build
make img-mount # docker compose up --build --detach
make img-unmount # docker compose down + remove the local image
make help lists every available target.
Testing
make pkg-test
# = swift test --disable-xctest --enable-code-coverage --enable-swift-testing --parallel
Tests use the Swift Testing framework. The Website.xctestplan covers two targets: WebsiteTests (the executable/integration tests) and WebsiteCoreTests (the library unit tests).
Deployment
The production image is built for linux/amd64 in release mode with a statically linked Swift runtime and jemalloc, runs as a non-root hummingbird user, and exposes port 8080 (ENTRYPOINT ./Website --http-host 0.0.0.0 --http-port 8080).
Build, tag, and push a release to the registry (an explicit version is required):
make img-release version=1.2.3
Pull and run the prebuilt image in production — the -f docker-compose.yml flag is important, as it skips the local-development override:
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d
Required variables
The Makefile and Compose files read these from a .env file (or the environment). Provide your own values — do not commit secrets.
| Variable | Used for |
|---|---|
HOST_CONTAINER |
Container registry host (e.g. registry.example.com). |
HOST_OWNER |
Registry namespace / owner. |
HOST_USER, HOST_PASSWORD |
Registry credentials for make img-release. |
IMAGE_NAME, IMAGE_TAG |
Image name and tag. |
IMAGE_PLATFORM |
Build platform (e.g. linux/amd64). |
HOST_PORT |
Host port mapped to the container's 8080 (default 8080). |
LOG_LEVEL |
Runtime log level (default info). |
HTTP_SERVER_NAME |
Runtime server name (default LoudWebsite). |
SECURITY_STRICT_TRANSPORT_SECURITY |
HSTS header value (default max-age=31536000; includeSubDomains). |