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,4 +1,6 @@
import CompressNIO
import Elementary
import Foundation
import HTTPTypes
import Hummingbird
import NIOCore
@@ -9,14 +11,14 @@ import NIOCore
/// precomputed headers instead of re-rendering. This suits pages whose markup never changes between requests, such as the landing page and the
/// not-found page, avoiding a per-request Elementary render on hot paths.
///
/// The bytes are gzipped once as well, so a page is compressed at startup rather than per request, and served sized rather than chunked. A client that
/// accepts no gzip gets the rendered bytes, leaving ``CompressionMiddleware`` to apply whatever encoding it did negotiate.
///
/// A successful page also revalidates cheaply: its headers carry a weak entity tag derived from the rendered bytes and a `Cache-Control` that asks
/// clients to revalidate (`no-cache`), so a repeat visit costs a `304 Not Modified` instead of a full transfer and a deploy that changes the page
/// changes the tag, propagating immediately.
/// changes the tag, propagating immediately. The tag is weak and spans both encodings, so a revalidation succeeds whichever copy the client holds.
///
/// ``LocalizedHTMLCollectionResponse`` builds on this type, caching one instance per supported language.
///
/// The body is written as an unsized stream (no `Content-Length`), mirroring `HTMLResponse`, so the response-compression middleware downstream
/// treats it exactly as it would a freshly rendered page.
public struct CachedHTMLResponse: Sendable {
// MARK: Properties
@@ -27,6 +29,9 @@ public struct CachedHTMLResponse: Sendable {
/// The weak entity tag of the rendered bytes, present on successful pages only.
private let eTag: String?
/// The page gzipped once, and the headers announcing it, or `nil` when the bytes did not compress.
private let gzip: (buffer: ByteBuffer, headers: HTTPFields)?
/// The headers applied to every response, precomputed once.
private let headers: HTTPFields
@@ -35,7 +40,7 @@ public struct CachedHTMLResponse: Sendable {
// MARK: Initializers
/// Renders the given document to bytes once.
/// Renders the given document to bytes once, and gzips them once.
///
/// A `200 OK` page gets the revalidation headers (`ETag` and `Cache-Control`); an error page does not, since a `304 Not Modified` only
/// ever stands in for a success.
@@ -74,16 +79,19 @@ public struct CachedHTMLResponse: Sendable {
self.eTag = eTag
self.headers = headers
self.status = status
// A page that will not compress is served as rendered.
self.gzip = Self.gzipped(
buffer,
headers: headers
)
}
// MARK: Methods
/// Builds a response from the cached, pre-rendered bytes.
///
/// A conditional request whose `If-None-Match` names the page's entity tag is answered with a
/// bodyless `304 Not Modified`. Otherwise the full page is served, mirroring the
/// `text/html; charset=utf-8` content type `HTMLResponse` produces and leaving the
/// `Content-Length` unset so small pages remain eligible for compression.
/// A conditional request whose `If-None-Match` names the page's entity tag is answered with a bodyless `304 Not Modified`. Otherwise the
/// page is served: the gzipped copy when the request accepts gzip, and the rendered bytes when it does not.
/// - Parameter request: the request the response answers.
/// - Returns: the response carrying the cached HTML body, or its `304` revalidation.
public func response(
@@ -101,10 +109,24 @@ public struct CachedHTMLResponse: Sendable {
)
}
guard
let gzip,
Self.acceptsGzip(request)
else {
return Response(
status: status,
headers: headers,
body: .init { [buffer] writer in
try await writer.write(buffer)
try await writer.finish(nil)
}
)
}
return Response(
status: status,
headers: headers,
body: .init { [buffer] writer in
headers: gzip.headers,
body: .init { [buffer = gzip.buffer] writer in
try await writer.write(buffer)
try await writer.finish(nil)
}
@@ -112,3 +134,122 @@ public struct CachedHTMLResponse: Sendable {
}
}
// MARK: - Helpers
private extension CachedHTMLResponse {
// MARK: Methods
/// Whether the request accepts a gzipped body.
///
/// Only gzip is precomputed: a request asking for another encoding alone falls through to the rendered bytes for ``CompressionMiddleware`` to
/// encode. A `q=0` is a refusal; the wildcard accepts on the client's behalf.
/// - Parameter request: the incoming request.
/// - Returns: `true` when the gzipped copy may be served.
static func acceptsGzip(
_ request: Request
) -> Bool {
var wildcard = false
for value in request.headers[values: .acceptEncoding] {
for entry in value.split(separator: .Separator.comma) {
let parts = entry.split(separator: .Separator.semicolon)
let name = parts.first?
.trimmingCharacters(in: .whitespaces)
.lowercased()
guard
let name,
name == .gzip || name == .xGzip || name == .wildcard
else {
continue
}
let isAccepted = quality(of: parts.dropFirst()) > 0
if name == .wildcard {
wildcard = isAccepted
} else if isAccepted {
return true
} else {
// An explicit `gzip;q=0` refuses it outright, whatever the wildcard says.
return false
}
}
}
return wildcard
}
/// The `q` weight carried by an `Accept-Encoding` entry's parameters; parameters without one are the highest preference.
/// - Parameter parameters: the entry's parameters, the coding name already dropped.
/// - Returns: the entry's weight.
static func quality(
of parameters: some Sequence<Substring>
) -> Double {
for parameter in parameters {
let parameter = parameter
.trimmingCharacters(in: .whitespaces)
.lowercased()
guard parameter.hasPrefix(.qualityPrefix) else {
continue
}
return Double(parameter.dropFirst(String.qualityPrefix.count)) ?? 0
}
return 1
}
/// Gzips the rendered bytes and builds the headers announcing them.
///
/// The copy is complete before the first byte is written, so it carries a `Content-Length` rather than being chunked. Compression that fails or does
/// not pay for itself yields `nil`.
/// - Parameters:
/// - buffer: the rendered bytes.
/// - headers: the headers the response carries before the encoding is announced.
/// - Returns: the compressed bytes and their headers, or `nil` when the page is better served uncompressed.
static func gzipped(
_ buffer: ByteBuffer,
headers: HTTPFields
) -> (buffer: ByteBuffer, headers: HTTPFields)? {
var source = buffer
guard
let compressed = try? source.compress(with: .gzip()),
compressed.readableBytes < buffer.readableBytes
else {
return nil
}
var headers = headers
headers[.contentEncoding] = .gzip
headers[.contentLength] = String(compressed.readableBytes)
return (compressed, headers)
}
}
// MARK: - Constants
private extension Character {
enum Separator {
static let comma: Character = ","
static let semicolon: Character = ";"
}
}
private extension String {
/// The content coding the pages are precompressed with.
static let gzip = "gzip"
/// The prefix of an `Accept-Encoding` entry's weight parameter.
static let qualityPrefix = "q="
/// The content coding some older clients spell `gzip` as.
static let xGzip = "x-gzip"
/// The `Accept-Encoding` entry accepting any coding on the client's behalf.
static let wildcard = "*"
}