# CCN Website The **CCN** public website service — a [Hummingbird](https://github.com/hummingbird-project/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](https://github.com/elementary-swift/elementary) and cached), plus one prefixed route per non-default catalog language — `GET /nl` and so on (see [Language editions](#language-editions)). - Negotiates each request's language from the `lang` query parameter, the leading path segment, then its `Accept-Language` header, against the languages in the `WebsiteLibrary` String Catalog, falling back to the default (`en`); pages are served from the per-language cache with `Content-Language` and `Vary: Accept-Language` headers. - Builds every page on the shared `Page` scaffolding from `Infrastructure`, which assembles the document head around the page's own markup: the viewport declaration, the optional `description` summary and `rel="canonical"` link, the Open Graph / Twitter link-preview tags, the JSON-LD structured-data script, and the optional analytics tracker (see [Page metadata](#page-metadata)). - Answers a liveness check at `GET /health` with a static JSON payload, and a readiness check at `GET /health/ready` that reports whether the database is reachable (`200` ready / `503` unavailable). - Answers `HEAD` on every `GET` route: the router is built with `.autoGenerateHeadEndpoints`, so uptime monitors and crawlers probing with `HEAD` get the route's status and headers instead of a `404`. - Serves static files (CSS, JS, icons, manifest, `robots.txt`, `sitemap.xml`) from `Resources/Static` via Hummingbird's `FileMiddleware`, tagged with media-type-specific `Cache-Control`; the production image ships minified copies (see [Static assets](#static-assets)). - Returns a custom not-found (404) HTML page, localized like the landing page, for any request that matches neither a route nor a static file. - Embeds a cookieless [Umami](https://umami.is) tracker on both pages once a deployment configures one; it ships **off**, so an unconfigured copy requests no third-party script (see [Analytics](#analytics)). - Compresses responses (gzip/deflate) above a configurable size when the client advertises support. - Stamps a hardened set of security headers on every response. - Persists data through [Fluent](https://github.com/hummingbird-project/hummingbird-fluent), against either an ephemeral in-memory SQLite database (the default — no external infrastructure) or a PostgreSQL server, selected by a single configuration key. ## Requirements - Swift 6.3 toolchain (`swift-tools-version:6.3`). - Docker (optional) for the containerized run/deploy workflow. - The [Hummingbird](https://github.com/hummingbird-project/hummingbird) CLI (`hb`) — optional, only for `make site-run` (watch and rebuild on change). ## Architecture Two SwiftPM targets: | Target | Kind | Path | Role | | --- | --- | --- | --- | | `Website` | executable | `Sources/App` | Entry point: reads configuration, builds the persistence service, and either serves the website or runs the migrate-and-exit mode. | | `WebsiteLibrary` | library | `Sources/Library` | Controllers, the pages (`IndexPage`, `NotFoundPage`) and their shared `Page` defaults, the `StaticFile` asset catalog, the request context, the String Catalog, and the `*+Defaults` extensions and configuration-key constants that supply the site's specifics to `Infrastructure`. | The `Website` executable depends on four local packages, each under `Packages/`: - `Localization` — the `Localize` and `Negotiate` helpers and the `LanguageList` of catalog languages (used by `WebsiteLibrary`). - `Infrastructure` — the shared Hummingbird toolkit: the `RouterController` protocol and `addController` result-builder extension for declarative routing, the security/vary/rate-limit/localization/not-found middlewares, the `Page` and `Asset` scaffolding, the `SocialCard`/`StructuredData`/`Analytics` head-metadata types, the pre-rendered localized HTML responses, and the `FingerprintAssets` version-token derivation. The service supplies its specifics (String Catalog bundle, pages, icon metadata, analytics) through the `*+Defaults` extensions in `WebsiteLibrary` and the `ConfigReader` properties in the executable. - `Persistence` — the Fluent-based data layer: the `Driver` selector, the `Service` factory that builds the `Fluent` service, the `PrepareDB` registrar that declares the migrations, and the `Probe` consulted by the readiness check; the models, migrations, and repositories stay internal to the package. It has no dependency on `swift-configuration`; the executable maps the `database.*` keys onto the driver. - `Utility` — small shared helpers with no server dependencies, currently the `NormalizeEmail` method. The persistence backend runs as a `Fluent` service inside the application's ServiceLifecycle group, so it starts and stops alongside the HTTP server (which owns its connection-pool shutdown on graceful termination). Requests pass through the middleware chain in this order (outermost first), then reach the routes: ``` LogRequestsMiddleware → SecurityHeadersMiddleware (security headers on every response) → HTTPSRedirectMiddleware (301 to site.origin when forwarded over plain HTTP) → TrailingSlashRedirectMiddleware (301 to the path without a trailing slash) → VaryMiddleware (marks every response as varying on Accept-Encoding) → ResponseCompressionMiddleware (gzip/deflate above the size threshold) → LocalizationMiddleware (negotiates the language: ?lang=, path prefix, then Accept-Language) → NotFoundMiddleware (renders the localized not-found page on .notFound) → FileMiddleware (serves Resources/Static) RootController (GET / → landing page; GET / → its other editions) HealthController (GET /health → liveness, GET /health/ready → readiness) ``` The router is created with `.autoGenerateHeadEndpoints`, so each of those `GET` routes gets a `HEAD` sibling for free. ### Page metadata Each page conforms to `Infrastructure`'s `Page` protocol and supplies only its `title`, `content`, `stylesheets`, and `scripts`; the protocol assembles the document around them and renders the head in a fixed order: the viewport declaration, the `analytics` origin preconnect hint, the `summary`, the `canonicalURL` link, the `socialCard` tags, the `structuredData` script, the `analytics` tracker script, then the page `metadata` and the stylesheet links. The body is the content followed by the script tags. Four of those are page-authored and optional. `IndexPage` supplies `canonicalURL`; the other three are **omitted by default** — the reference site leaves them unset, so a generated site fills in what it needs by overriding them on `IndexPage` (or on the shared `Page+Defaults` extension, for site-wide values): | Property | Renders as | Notes | | --- | --- | --- | | `summary` | `` | The page's one-line description. | | `canonicalURL` | `` | Absolute URL. `IndexPage` derives it from `site.origin` and the page's own language; an unset origin omits it (see [Language editions](#language-editions)). | | `socialCard` | Open Graph + Twitter `` tags | A `SocialCard` — title, summary, URL, site name, locale, alternate locales, share image. Scrapers require absolute URLs, so the page composes them from its own origin; `Page+Defaults` supplies the locale as `ogLocale`. | | `structuredData` | `