Files
ccn/Services/Website
javier 847058d642 Optimizations for the Website service (#9)
This PR contains the work done to provide optimizations to the current service, such as a health-check endpoint, pre-renders static HTML pages, and hardens the error page's CSP.

To provide further details about the work:

* Added the `HealthController` controller serving GET `/health` with a static JSON payload.
* Added the `CachedHTMLResponse` response, which renders a static HTMLDocument to bytes once and reuses them per request (no Content-Length, so responses stay compressible).
* Integrated the response into the `RootController` and the  `NotFoundMiddleware` middleware to avoid re-rendering on hot paths.
* Added a `RouterMethods.addRoutes(_:)` extension and switched the router in App+build to use it.
* Moved the inline style from the `ErrorPage` page into a dedicated style file so the CSP needs no inline-style escape hatch.
* Fixed the `IndexPage` page path inconsistencies.
* Written the `README` file.

Reviewed-on: rock-n-code/loud-amsterdam#9
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
2026-06-28 14:18:04 +00:00
..
2026-06-27 02:26:33 +00:00

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 with Elementary and cached).
  • Serves static files (CSS, JS, icons, manifest, robots.txt) from Resources/Static via Hummingbird's FileMiddleware, tagged with media-type-specific Cache-Control.
  • Returns a custom HTML 404 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.

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)
      → NotFoundMiddleware          (renders the 404 page on .notFound)
        → FileMiddleware            (serves Resources/Static)
RootController                      (GET / → landing page)

Configuration

Configuration is read through swift-configuration from the following sources, highest precedence first:

  1. Command-line arguments (e.g. --http-host 0.0.0.0)
  2. Process environment variables
  3. A .env file in the working directory (optional)
  4. 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.serverNameHTTP_SERVER_NAME, security.strictTransportSecuritySECURITY_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 setsLOG_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. TheWebsite.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).