Project updates from Template

This commit contains the latest updates from the generic Website template, which rework the compression and localization:

- Reworked the compression and localization in the Infrastructure package. (3c568e4)
- Adopted the reworked compression and localization in the Website service. (08cf3e3)

The template commit that only touched the root README (53676ed) was left out, as this project no longer carries that file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 12:45:58 +02:00
co-authored by Claude Fable 5.1
parent 65b62681eb
commit 916df7e2f0
24 changed files with 426 additions and 218 deletions
@@ -1,6 +1,5 @@
import Configuration
import Hummingbird
import HummingbirdCompression
import Localization
import Logging
import Persistence
@@ -140,19 +139,11 @@ private func logger(
/// Builds the application's router.
///
/// Registers the request-logging middleware, the security-headers middleware that stamps the given `securityHeaders` onto every response, the
/// HTTPS-redirect middleware that bounces requests forwarded over plain HTTP to the canonical origin, the trailing-slash redirect middleware that
/// collapses each path onto its canonical form, the vary middleware that marks every response as varying on `Accept-Encoding`, the response-compression middleware that compresses responses
/// larger than `minimumResponseSizeToCompress` when the client advertises support, the localization middleware that negotiates the request's
/// language from its `Accept-Language` header (honouring the `lang` query override and the language a leading path segment names), the
/// not-found middleware that serves the not-found page, and the static file middleware that serves the
/// contents of `staticFilesPath` (tagging responses with the given `cacheControl` directives), then adds the `RootController` routes that
/// render the landing page one per language the String Catalog serves and the `HealthController` routes that serve the health check.
///
/// The security-headers middleware sits just inside request logging so it covers every response that reaches a client the landing page, the compressed
/// responses, the rendered not-found page, and the served static files. The HTTPS redirect sits directly beneath it, so a redirect carries the security
/// headers but skips the negotiation, compression, and file lookup it would otherwise pay for. The trailing-slash redirect follows it, ahead of the
/// routes and `FileMiddleware` that would otherwise answer both spellings of every path.
/// The chain below reads as its own list; the order is what does not. Security headers sit just inside request logging, so they reach every response a
/// client sees pages, compressed bodies, the not-found page, the served files. The HTTPS redirect sits directly beneath, keeping those headers while
/// skipping the compression and file lookup it would otherwise pay for. The trailing-slash redirect follows, ahead of the routes and `FileMiddleware`
/// that would otherwise answer both spellings of every path. The language is negotiated by the responders that read it the landing route and the
/// not-found middleware rather than on every request past.
/// - Parameters:
/// - staticFilesPath: the folder, relative to the working directory, the static files are served from.
/// - assetVersion: the version token the pages append to their asset URLs, or `nil` to leave them unversioned.
@@ -196,10 +187,9 @@ private func router(
)
TrailingSlashRedirectMiddleware()
VaryMiddleware()
ResponseCompressionMiddleware(
CompressionMiddleware(
minimumResponseSizeToCompress: compressionMinResponseSize
)
LocalizationMiddleware()
NotFoundMiddleware(
assetVersion: assetVersion,
analytics: analytics
@@ -4,16 +4,13 @@ import NIOCore
/// The website's request context.
///
/// Extends the core request storage with the negotiated language, defaulting to the default supported language until ``LocalizationMiddleware``
/// resolves it from the request, and with the connected client's address, so ``RateLimitMiddleware`` can key its budgets per client.
public struct WebsiteRequestContext: LocalizedRequestContext, RemoteAddressRequestContext {
/// Extends the core request storage with the connected client's address, so ``RateLimitMiddleware`` can key its budgets per client.
public struct WebsiteRequestContext: RemoteAddressRequestContext {
// MARK: Properties
/// The core request context storage Hummingbird requires.
public var coreContext: CoreRequestContextStorage
/// The language identifier negotiated for the request.
public var language: String
/// The address of the connected client, captured from the source channel.
public let remoteAddress: SocketAddress?
@@ -25,14 +22,7 @@ public struct WebsiteRequestContext: LocalizedRequestContext, RemoteAddressReque
source: Source,
) {
self.coreContext = .init(source: source)
self.language = .empty
self.remoteAddress = source.channel.remoteAddress
}
}
// MARK: - Constants
private extension String {
static let empty = ""
}
@@ -1,6 +1,7 @@
import Foundation
import Hummingbird
import Infrastructure
import Localization
/// Serves the website's root routes.
///
@@ -13,10 +14,13 @@ import Infrastructure
/// ```
///
/// - Note: `Context` is the request context the routes are resolved against, and must match the context of the router the routes are added to.
public struct RootController<Context: LocalizedRequestContext> {
public struct RootController<Context: RequestContext> {
// MARK: Properties
/// Negotiates the language the bare route answers in.
private let negotiate: Negotiate
/// The landing page, rendered once per supported language and reused for every request.
private let responses: LocalizedHTMLCollectionResponse
@@ -32,7 +36,13 @@ public struct RootController<Context: LocalizedRequestContext> {
siteOrigin: String? = nil,
analytics: Analytics? = nil
) {
self.responses = .init(bundle: .module) {
self.negotiate = .init(bundle: .module)
// The bare route negotiates, so the pages declare `Vary: Accept-Language`; the prefixed editions share the cache
// and carry it too.
self.responses = .init(
bundle: .module,
variesOnAcceptLanguage: true
) {
IndexPage(
locale: $0,
assetVersion: assetVersion,
@@ -80,26 +90,27 @@ private extension RootController {
/// Handles a request for the landing page.
///
/// Renders the ``IndexPage`` in the language stored on the context by ``LocalizationMiddleware``, falling back to the default language.
/// Renders the ``IndexPage`` in the language negotiated from the request its `lang` query parameter, then the leading path segment, then
/// `Accept-Language` falling back to the default language.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
/// - Returns: the cached ``IndexPage`` response for the context's language.
/// - Returns: the cached ``IndexPage`` response for the negotiated language.
@Sendable
func index(
request: Request,
context: Context
) -> Response {
responses.response(
for: context.language,
for: negotiate(for: request),
request: request
)
}
/// Builds the handler serving the landing page in one fixed language, for the routes carrying the language in their path.
///
/// The path *is* the language choice, so the negotiated context language is ignored: a prefixed URL answers in its language for every
/// visitor and every crawler alike, which is what lets a search engine index it as that edition.
/// The path *is* the language choice, so nothing is negotiated: a prefixed URL answers in its language for every visitor and every crawler
/// alike, which is what lets a search engine index it as that edition.
/// - Parameter language: the language the route serves.
/// - Returns: the handler answering requests for that edition of the page.
func index(
@@ -1,13 +0,0 @@
import Foundation
import Infrastructure
public extension LocalizationMiddleware {
// MARK: Initializers
/// Creates a localization middleware that negotiates against the module's String Catalog languages.
init() {
self.init(bundle: .module)
}
}