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>
138 lines
4.5 KiB
Swift
138 lines
4.5 KiB
Swift
import Foundation
|
|
import Hummingbird
|
|
import Infrastructure
|
|
import Localization
|
|
|
|
/// Serves the website's root routes.
|
|
///
|
|
/// The controller exposes its routes through its `RouterController` conformance, so the application that composes it registers them declaratively:
|
|
///
|
|
/// ```swift
|
|
/// router.addController {
|
|
/// RootController<AppRequestContext>()
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - 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: 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
|
|
|
|
// MARK: Initializers
|
|
|
|
/// Creates a root controller.
|
|
/// - Parameters:
|
|
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
|
|
/// - siteOrigin: the public origin the page derives its canonical URL and language alternates from, or `nil` (the default) to omit them.
|
|
/// - analytics: the analytics tracker the landing page embeds, or `nil` (the default) to omit it.
|
|
public init(
|
|
assetVersion: String? = nil,
|
|
siteOrigin: String? = nil,
|
|
analytics: Analytics? = nil
|
|
) {
|
|
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,
|
|
siteOrigin: siteOrigin,
|
|
analytics: analytics
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - RouteController
|
|
|
|
extension RootController: RouterController {
|
|
|
|
// MARK: Properties
|
|
|
|
public var routes: RouteCollection<Context> {
|
|
let routes = RouteCollection(context: Context.self)
|
|
|
|
routes.get(
|
|
.Root.index,
|
|
use: index
|
|
)
|
|
|
|
// Every non-default language answers under a prefix of its own, so the `hreflang` alternates the page advertises
|
|
// resolve and a crawler can index each edition at a stable URL. A single-language catalog adds none.
|
|
for language in Language.all where !language.isDefault {
|
|
routes.get(
|
|
.init(language.path(IndexPage.path)),
|
|
use: index(in: language)
|
|
)
|
|
}
|
|
|
|
return routes
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private extension RootController {
|
|
|
|
// MARK: Methods
|
|
|
|
/// Handles a request for the landing page.
|
|
///
|
|
/// 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 negotiated language.
|
|
@Sendable
|
|
func index(
|
|
request: Request,
|
|
context: Context
|
|
) -> Response {
|
|
responses.response(
|
|
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 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(
|
|
in language: Language
|
|
) -> @Sendable (Request, Context) -> Response {
|
|
{ request, _ in
|
|
responses.response(
|
|
for: language.identifier,
|
|
request: request
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - Constants
|
|
|
|
private extension RouterPath {
|
|
/// A namespace for the ``RootController`` route paths.
|
|
enum Root {
|
|
/// The path of the landing page; the page builds its canonical URL from the same constant.
|
|
static let index: RouterPath = .init(IndexPage.path)
|
|
}
|
|
}
|