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:
@@ -0,0 +1,70 @@
|
||||
import HTTPTypes
|
||||
import Hummingbird
|
||||
import HummingbirdCompression
|
||||
import Logging
|
||||
|
||||
/// Compresses the responses that are not already encoded, and passes the ones that are through untouched.
|
||||
///
|
||||
/// Hummingbird's `ResponseCompressionMiddleware` appends to `Content-Encoding` without checking for one, so a pre-compressed page (see
|
||||
/// ``CachedHTMLResponse``) would go out as `gzip, gzip` — a client decodes once and renders the inner gzip stream. This stands in for that
|
||||
/// middleware and delegates to it, so the threshold, the negotiation and the compressor stay its behaviour.
|
||||
///
|
||||
/// - Note: `Context` is the request context the middleware is resolved against.
|
||||
public struct CompressionMiddleware<Context: RequestContext>: Sendable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The middleware the unencoded responses are handed to.
|
||||
private let compression: ResponseCompressionMiddleware<Context>
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a compression middleware.
|
||||
/// - Parameter minimumResponseSizeToCompress: the smallest response body, in bytes, that is compressed at all.
|
||||
public init(
|
||||
minimumResponseSizeToCompress: Int
|
||||
) {
|
||||
self.compression = .init(
|
||||
minimumResponseSizeToCompress: minimumResponseSizeToCompress
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RouterMiddleware
|
||||
|
||||
extension CompressionMiddleware: RouterMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Passes the request down the chain and compresses the response on the way back up, unless it already names an encoding.
|
||||
/// - Parameters:
|
||||
/// - request: the incoming request.
|
||||
/// - context: the context the request is resolved against.
|
||||
/// - next: the next responder in the middleware chain.
|
||||
/// - Returns: the downstream response, compressed when it was not already.
|
||||
/// - Throws: any error thrown downstream.
|
||||
public func handle(
|
||||
_ request: Request,
|
||||
context: Context,
|
||||
next: (Request, Context) async throws -> Response
|
||||
) async throws -> Response {
|
||||
let response = try await next(
|
||||
request,
|
||||
context
|
||||
)
|
||||
|
||||
guard response.headers[.contentEncoding] == nil else {
|
||||
return response
|
||||
}
|
||||
|
||||
// The response is already in hand, so the delegate gets it rather than the chain — `next` runs once.
|
||||
return try await compression.handle(
|
||||
request,
|
||||
context: context
|
||||
) { _, _ in
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import Foundation
|
||||
import HTTPTypes
|
||||
import Hummingbird
|
||||
import Localization
|
||||
|
||||
/// Resolves the visitor's preferred language and records it on the request context.
|
||||
///
|
||||
/// Placed ahead of the localized responders in the middleware chain, it reads the request's `lang` query parameter, its path, and its
|
||||
/// `Accept-Language` header, negotiates the best supported match (falling back to the default language), and stores it on the context's
|
||||
/// ``LocalizedRequestContext/language``.
|
||||
///
|
||||
/// The query parameter is a deliberate override; failing that, a leading path segment naming a supported language pins it, so an unrouted path under
|
||||
/// a language's prefix — its not-found page — answers in that language. Values naming no supported language are ignored, leaving the header. The
|
||||
/// request is passed through untouched — the path and routing are unaffected.
|
||||
///
|
||||
/// A site whose page routes pin their language by URL consults this only for responses that belong to no URL — in practice, its not-found page.
|
||||
public struct LocalizationMiddleware<Context: LocalizedRequestContext> {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Negotiates the request's language from its `Accept-Language` header.
|
||||
private let negotiate: Negotiate
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a localization middleware that negotiates against the given bundle's String Catalog languages.
|
||||
/// - Parameter bundle: the bundle whose String Catalog names the supported languages.
|
||||
public init(
|
||||
bundle: Bundle
|
||||
) {
|
||||
self.negotiate = .init(bundle: bundle)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - RouterMiddleware
|
||||
|
||||
extension LocalizationMiddleware: RouterMiddleware {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Negotiates the request's language and records it on the context before passing it down.
|
||||
/// - Parameters:
|
||||
/// - request: the incoming request.
|
||||
/// - context: the context the request is resolved against.
|
||||
/// - next: the next responder in the middleware chain.
|
||||
/// - Returns: the downstream response.
|
||||
/// - Throws: any error thrown downstream.
|
||||
public func handle(
|
||||
_ request: Request,
|
||||
context: Context,
|
||||
next: (Request, Context) async throws -> Response
|
||||
) async throws -> Response {
|
||||
var context = context
|
||||
|
||||
// The deliberate query override first; failing that, the leading path segment, so a language's whole URL
|
||||
// prefix — routed or not — answers in its language.
|
||||
let requested = request.uri.queryParameters[.Parameter.language].map(String.init)
|
||||
?? request.uri.path.split(separator: "/").first.map(String.init)
|
||||
|
||||
context.language = negotiate(
|
||||
requested: requested,
|
||||
acceptLanguage: request.headers[.acceptLanguage]
|
||||
)
|
||||
|
||||
return try await next(request, context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private extension Substring {
|
||||
/// A namespace for the query parameters the middleware reads.
|
||||
enum Parameter {
|
||||
/// The query parameter carrying an explicit language choice; the site's language switcher appends it to the current path.
|
||||
static let language: Substring = "lang"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
import Elementary
|
||||
import Foundation
|
||||
import Hummingbird
|
||||
import Localization
|
||||
|
||||
/// Serves a custom error page for requests that match neither a route nor a static file.
|
||||
///
|
||||
/// Placed ahead of `FileMiddleware` in the middleware chain, it catches the `.notFound` error that bubbles up when no file exists for the requested
|
||||
/// path and responds with the rendered error page and a `404 Not Found` status. The page is served in the language stored on the context by
|
||||
/// ``LocalizationMiddleware``, falling back to the default language.
|
||||
/// path and responds with the rendered error page and a `404 Not Found` status.
|
||||
///
|
||||
/// Its responses declare `Vary: Accept-Language`: where the page routes pin their language by URL, this is the one responder that negotiates.
|
||||
/// An unrouted path names no edition, so no canonical URL contradicts the header.
|
||||
public struct NotFoundMiddleware<Context: LocalizedRequestContext> {
|
||||
/// It negotiates the language itself, on the way out, so the cost falls on the 404s rather than on every request through the chain — where page
|
||||
/// routes pin their language by URL, this is the one responder that has to ask. Its responses declare `Vary: Accept-Language` accordingly: an
|
||||
/// unrouted path names no edition, so no canonical URL contradicts the header.
|
||||
public struct NotFoundMiddleware<Context: RequestContext> {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// Negotiates the language a not-found response is served in.
|
||||
private let negotiate: Negotiate
|
||||
|
||||
/// The error page, rendered once per supported language and reused for every not-found response.
|
||||
private let responses: LocalizedHTMLCollectionResponse
|
||||
|
||||
@@ -27,6 +31,7 @@ public struct NotFoundMiddleware<Context: LocalizedRequestContext> {
|
||||
bundle: Bundle,
|
||||
document: (Locale) -> Document
|
||||
) {
|
||||
self.negotiate = .init(bundle: bundle)
|
||||
self.responses = .init(
|
||||
bundle: bundle,
|
||||
status: .notFound,
|
||||
@@ -69,7 +74,7 @@ extension NotFoundMiddleware: RouterMiddleware {
|
||||
}
|
||||
|
||||
return responses.response(
|
||||
for: context.language,
|
||||
for: negotiate(for: request),
|
||||
request: request
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user