Initial commit.

This commit is contained in:
2026-08-19 23:19:08 +02:00
commit 22e737d9c2
153 changed files with 11680 additions and 0 deletions
@@ -0,0 +1,61 @@
{
"sourceLanguage" : "en",
"strings" : {
"index.greeting" : {
"comment" : "The landing page's greeting paragraph.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hello world! This is HTML5 Boilerplate."
}
}
}
},
"index.title" : {
"comment" : "The landing page's document title.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Index page"
}
}
}
},
"notFound.heading" : {
"comment" : "The not-found page's main heading.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Page Not Found"
}
}
}
},
"notFound.message" : {
"comment" : "The not-found page's body text.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sorry, but the page you were trying to view does not exist."
}
}
}
},
"notFound.title" : {
"comment" : "The not-found page's document title.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Page Not Found"
}
}
}
}
},
"version" : "1.0"
}
@@ -0,0 +1,72 @@
import Infrastructure
/// A static file shipped with the website service.
///
/// Each case identifies a file name stored under the static files root (the `Resources/Static` directory) and served by Hummingbird's
/// `FileMiddleware` middleware. A name can be available with more than one extension (see ``fileExtensions``), each resolving to its own file.
enum StaticFile: Asset, CaseIterable {
/// The `apple-touch-icon.png` icon.
case appleTouchIcon
/// The `favicon.ico` icon.
case favicon
/// The `icon.svg` icon.
case icon
/// The `icon-192.png` icon for the web manifest.
case icon192
/// The `icon-512.png` icon for the web manifest.
case icon512
/// The `css/index.css` stylesheet and `js/index.js` script for the landing page.
case index
/// The `css/not-found.css` stylesheet and `js/not-found.js` script for the not-found page.
case notFound
/// The `robots.txt` crawler directives.
case robots
/// The `css/shared.css` stylesheet and `js/shared.js` script shared across pages.
case shared
/// The `site.webmanifest` web application manifest.
case site
/// The `sitemap.xml` crawler sitemap.
case sitemap
}
// MARK: - Extensions
extension StaticFile {
// MARK: Computed
/// The file extensions the file is available with.
var fileExtensions: [AssetExtension] {
switch self {
case .appleTouchIcon,
.icon192,
.icon512: [.png]
case .index,
.notFound,
.shared: [.css, .js]
case .favicon: [.ico]
case .icon: [.svg]
case .robots: [.txt]
case .site: [.webmanifest]
case .sitemap: [.xml]
}
}
/// The file's name, without extension.
var fileName: String {
switch self {
case .appleTouchIcon: "apple-touch-icon"
case .favicon: "favicon"
case .icon: "icon"
case .icon192: "icon-192"
case .icon512: "icon-512"
case .index: "index"
case .notFound: "not-found"
case .robots: "robots"
case .shared: "shared"
case .site: "site"
case .sitemap: "sitemap"
}
}
}
@@ -0,0 +1,70 @@
import Elementary
import Foundation
import Infrastructure
import Localization
/// The site-wide defaults shared by every page of the website.
extension Page {
// MARK: Computed
/// The document language, derived from the page's locale and falling back to the default language.
var lang: String {
locale.language.languageCode?.identifier
?? LanguageList().default
}
/// The icon, manifest, and theme colour metadata shared by every page of the website.
@HTMLBuilder
var metadata: some HTML {
link(
.rel(.icon),
.href(StaticFile.favicon.urlPath(
for: .ico,
version: assetVersion
)),
.custom(
name: "sizes",
value: "any"
)
)
link(
.rel(.icon),
.href(StaticFile.icon.urlPath(
for: .svg,
version: assetVersion
)),
.custom(
name: "type",
value: "image/svg+xml"
)
)
link(
.rel("apple-touch-icon"),
.href(StaticFile.appleTouchIcon.urlPath(
for: .png,
version: assetVersion
))
)
link(
.rel("manifest"),
.href(StaticFile.site.urlPath(
for: .webmanifest,
version: assetVersion
))
)
meta(
.name("theme-color"),
.content("#fafafa")
)
meta(
.name("theme-color"),
.content("#0c0710"),
.custom(
name: "media",
value: "(prefers-color-scheme: dark)"
)
)
}
}
@@ -0,0 +1,67 @@
import Elementary
import Foundation
import Infrastructure
import Localization
/// The website's landing page, with its text localized to a given locale.
struct IndexPage {
// MARK: Properties
/// The analytics tracker embedded as a deferred script in the document head, or `nil` to omit it.
let analytics: Analytics?
/// The version token appended to the page's asset URLs, or `nil` to leave them unversioned.
let assetVersion: String?
/// The locale the page content is localized to.
let locale: Locale
/// Resolves the page's text from the bundled String Catalog for the page's ``locale``.
private let localize: Localize
// MARK: Initializers
/// Creates a landing page localized to the given locale.
/// - Parameters:
/// - locale: the locale the page content is localized to.
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
/// - analytics: the analytics tracker embedded in the document head, or `nil` (the default) to omit it.
init(
locale: Locale,
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.analytics = analytics
self.assetVersion = assetVersion
self.locale = locale
self.localize = .init(bundle: .module)
}
}
// MARK: - Page
extension IndexPage: Page {
// MARK: Properties
var content: some HTML {
p {
localize("index.greeting", locale: locale)
}
}
var scripts: [any Asset] {
[StaticFile.index, StaticFile.shared]
}
var stylesheets: [any Asset] {
[StaticFile.shared, StaticFile.index]
}
var title: String {
localize("index.title", locale: locale)
}
}
@@ -0,0 +1,70 @@
import Elementary
import Foundation
import Infrastructure
import Localization
/// The HTML page rendered for a not-found response, with its text localized to a given locale.
struct NotFoundPage {
// MARK: Properties
/// The analytics tracker embedded as a deferred script in the document head, or `nil` to omit it.
let analytics: Analytics?
/// The version token appended to the page's asset URLs, or `nil` to leave them unversioned.
let assetVersion: String?
/// The locale the page content is localized to.
let locale: Locale
/// Resolves the page's text from the bundled String Catalog for the page's ``locale``.
private let localize: Localize
// MARK: Initializers
/// Creates a not-found page localized to the given locale.
/// - Parameters:
/// - locale: the locale the page content is localized to.
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the
/// default) to leave them unversioned.
/// - analytics: the analytics tracker embedded in the document head, or `nil` (the default) to omit it.
init(
locale: Locale,
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.analytics = analytics
self.assetVersion = assetVersion
self.locale = locale
self.localize = .init(bundle: .module)
}
}
// MARK: Page
extension NotFoundPage: Page {
// MARK: Properties
var content: some HTML {
h1 {
localize("notFound.heading", locale: locale)
}
p {
localize("notFound.message", locale: locale)
}
}
var scripts: [any Asset] {
[StaticFile.notFound, StaticFile.shared]
}
var stylesheets: [any Asset] {
[StaticFile.shared, StaticFile.notFound]
}
var title: String {
localize("notFound.title", locale: locale)
}
}
@@ -0,0 +1,38 @@
import Hummingbird
import Infrastructure
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 {
// 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?
// MARK: Initializers
/// Creates a request context for the given source.
/// - Parameter source: the source the context is initialized from.
public init(
source: Source,
) {
self.coreContext = .init(source: source)
self.language = .empty
self.remoteAddress = source.channel.remoteAddress
}
}
// MARK: - Constants
private extension String {
static let empty = ""
}
@@ -0,0 +1,156 @@
import Hummingbird
import NIOCore
import Persistence
import Infrastructure
/// Serves the website's health-check routes.
///
/// The controller exposes its routes through its `RouterController` conformance, so the application that composes it registers them declaratively:
///
/// ```swift
/// router.addController {
/// HealthController<AppRequestContext>(probe: probe)
/// }
/// ```
///
/// It always serves a liveness check at `/health`; when a `Probe` is supplied it also serves a readiness check at `/health/ready` that reports
/// whether the service's database is reachable. The two are kept distinct so an orchestrator can restart on liveness failure but only withhold traffic on
/// readiness failure.
///
/// - 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 HealthController<Context: RequestContext> {
// MARK: Properties
/// The probe consulted for the readiness check, or `nil` when only liveness is served.
private let probe: Probe?
// MARK: Initializers
/// Creates a health controller.
/// - Parameter probe: the probe consulted by the readiness route; when `nil`, only the liveness route is served.
public init(
probe: Probe? = nil
) {
self.probe = probe
}
}
// MARK: - RouterController
extension HealthController: RouterController {
// MARK: Properties
public var routes: RouteCollection<Context> {
let routes = RouteCollection(context: Context.self)
routes.get(
.Health.check,
use: check
)
if probe != nil {
routes.get(
.Health.ready,
use: ready
)
}
return routes
}
}
// MARK: - Helpers
private extension HealthController {
// MARK: Methods
/// Handles a request for the liveness check.
///
/// Returns a constant JSON body built directly per request the payload is a tiny literal with no rendering step, so there is nothing to pre-render or
/// cache. It reports only that the process is up, with no dependency check, so an orchestrator restarts the process only when the process itself is
/// unresponsive.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
/// - Returns: a `200 OK` response carrying the static JSON status payload.
@Sendable
func check(
request: Request,
context: some RequestContext
) -> Response {
json(
status: .ok,
payload: .Payload.live
)
}
/// Handles a request for the readiness check.
///
/// Consults the `Probe` supplied at initialization and reports `200 OK` when the service's database is reachable, or `503 Service Unavailable`
/// otherwise, so a load balancer withholds traffic from an instance that cannot yet serve it without restarting the process.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
/// - Returns: a `200 OK` response when ready, or `503 Service Unavailable` when not.
@Sendable
func ready(
request: Request,
context: some RequestContext
) async -> Response {
guard await probe?() == true else {
return json(
status: .serviceUnavailable,
payload: .Payload.unavailable
)
}
return json(
status: .ok,
payload: .Payload.ready
)
}
/// Builds a JSON response carrying the given status and payload.
/// - Parameters:
/// - status: the HTTP status of the response.
/// - payload: the JSON body of the response.
/// - Returns: the configured JSON response.
func json(
status: HTTPResponse.Status,
payload: String
) -> Response {
Response(
status: status,
headers: [.contentType: "application/json"],
body: .init(byteBuffer: .init(string: payload))
)
}
}
// MARK: - RouterPath+Constants
private extension RouterPath {
/// A namespace for the ``HealthController`` route paths.
enum Health {
/// The path of the liveness endpoint.
static let check: RouterPath = "/health"
/// The path of the readiness endpoint.
static let ready: RouterPath = "/health/ready"
}
}
// MARK: - String+Constants
private extension String {
enum Payload {
static let live = #"{"status":"ok"}"#
static let ready = #"{"status":"ready"}"#
static let unavailable = #"{"status":"unavailable"}"#
}
}
@@ -0,0 +1,97 @@
import Foundation
import Hummingbird
import Infrastructure
/// 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: LocalizedRequestContext> {
// MARK: Properties
/// 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.
/// - analytics: the analytics tracker the landing page embeds, or `nil` (the default) to omit it.
public init(
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.responses = .init(bundle: .module) {
IndexPage(
locale: $0,
assetVersion: assetVersion,
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
)
return routes
}
}
// MARK: - Helpers
private extension RootController {
// MARK: Methods
/// 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.
/// - Parameters:
/// - request: the incoming request.
/// - context: the context the request is resolved against.
/// - Returns: the cached ``IndexPage`` response for the context's language.
@Sendable
func index(
request: Request,
context: Context
) -> Response {
responses.response(
for: context.language,
request: request
)
}
}
// MARK: - Constants
private extension RouterPath {
/// A namespace for the ``RootController`` route paths.
enum Root {
/// The path of the landing page.
static let index: RouterPath = "/"
}
}
@@ -0,0 +1,95 @@
import Configuration
extension AbsoluteConfigKey {
/// A namespace for the analytics configuration keys, as absolute keys.
public enum Analytics {
/// The absolute configuration key for the analytics website identifier.
public static let websiteID: AbsoluteConfigKey = .init(.Analytics.websiteID)
/// The absolute configuration key for the comma-delimited domains the tracker reports from.
public static let domains: AbsoluteConfigKey = .init(.Analytics.domains)
/// The absolute configuration key for recorder mode, loading the session recorder script alongside the tracker.
public static let recorder: AbsoluteConfigKey = .init(.Analytics.recorder)
}
/// A namespace for the static files cache configuration keys, as absolute keys.
public enum Cache {
/// The absolute configuration key for the max-age, in seconds, applied to fingerprinted assets and fonts.
public static let maxAgeAsset: AbsoluteConfigKey = .init(.Cache.maxAgeAsset)
/// The absolute configuration key for the max-age, in seconds, applied to unversioned text-based static files.
public static let maxAgeText: AbsoluteConfigKey = .init(.Cache.maxAgeText)
/// The absolute configuration key for the max-age, in seconds, applied to image static files.
public static let maxAgeImage: AbsoluteConfigKey = .init(.Cache.maxAgeImage)
/// The absolute configuration key for the max-age, in seconds, applied to all other static files.
public static let maxAgeDefault: AbsoluteConfigKey = .init(.Cache.maxAgeDefault)
}
/// A namespace for the response compression configuration keys, as absolute keys.
public enum Compression {
/// The absolute configuration key for the minimum response body size, in bytes, before compression is applied.
public static let minResponseSize: AbsoluteConfigKey = .init(.Compression.minResponseSize)
}
/// A namespace for the persistence configuration keys, as absolute keys.
public enum Database {
/// The absolute configuration key selecting migrate-and-exit mode.
public static let migrate: AbsoluteConfigKey = .init(.Database.migrate)
/// The absolute configuration key for the persistence driver.
public static let driver: AbsoluteConfigKey = .init(.Database.driver)
/// The absolute configuration key for the PostgreSQL host.
public static let host: AbsoluteConfigKey = .init(.Database.host)
/// The absolute configuration key for the PostgreSQL port.
public static let port: AbsoluteConfigKey = .init(.Database.port)
/// The absolute configuration key for the database name.
public static let name: AbsoluteConfigKey = .init(.Database.name)
/// The absolute configuration key for the database username.
public static let username: AbsoluteConfigKey = .init(.Database.username)
/// The absolute configuration key for the database password.
public static let password: AbsoluteConfigKey = .init(.Database.password)
/// The absolute configuration key for the TLS posture used when connecting.
public static let tls: AbsoluteConfigKey = .init(.Database.tls)
/// The absolute configuration key for the maximum pooled connections per event loop.
public static let poolMaxPerEventLoop: AbsoluteConfigKey = .init(.Database.poolMaxPerEventLoop)
/// The absolute configuration key for the longest wait, in seconds, for a pooled connection to become available.
public static let poolTimeout: AbsoluteConfigKey = .init(.Database.poolTimeout)
}
/// A namespace for the HTTP server configuration keys, as absolute keys.
public enum HTTP {
/// The absolute configuration key for the host the server binds to.
public static let host: AbsoluteConfigKey = .init(.HTTP.host)
/// The absolute configuration key for the port the server listens on.
public static let port: AbsoluteConfigKey = .init(.HTTP.port)
/// The absolute configuration key for the server's name.
public static let serverName: AbsoluteConfigKey = .init(.HTTP.serverName)
}
/// A namespace for the logging configuration keys, as absolute keys.
public enum Log {
/// The absolute configuration key for the minimum log level.
public static let level: AbsoluteConfigKey = .init(.Log.level)
}
/// A namespace for the rate limit configuration keys, as absolute keys.
public enum RateLimit {
/// The absolute configuration key for the number of requests admitted per client per window.
public static let limit: AbsoluteConfigKey = .init(.RateLimit.limit)
/// The absolute configuration key for the window length, in seconds.
public static let window: AbsoluteConfigKey = .init(.RateLimit.window)
/// The absolute configuration key for keying clients by the first `X-Forwarded-For` entry.
public static let trustForwardedFor: AbsoluteConfigKey = .init(.RateLimit.trustForwardedFor)
}
/// A namespace for the path configuration keys, as absolute keys.
public enum Path {
/// The absolute configuration key for the directory the static files are served from.
public static let staticFiles: AbsoluteConfigKey = .init(.Path.staticFiles)
}
/// A namespace for the security headers configuration keys, as absolute keys.
public enum Security {
/// The absolute configuration key for the `Content-Security-Policy` header value.
public static let contentSecurityPolicy: AbsoluteConfigKey = .init(.Security.contentSecurityPolicy)
/// The absolute configuration key for the `X-Content-Type-Options` header value.
public static let contentTypeOptions: AbsoluteConfigKey = .init(.Security.contentTypeOptions)
/// The absolute configuration key for the `X-Frame-Options` header value.
public static let frameOptions: AbsoluteConfigKey = .init(.Security.frameOptions)
/// The absolute configuration key for the `Referrer-Policy` header value.
public static let referrerPolicy: AbsoluteConfigKey = .init(.Security.referrerPolicy)
/// The absolute configuration key for the `Permissions-Policy` header value.
public static let permissionsPolicy: AbsoluteConfigKey = .init(.Security.permissionsPolicy)
/// The absolute configuration key for the `Strict-Transport-Security` header value.
public static let strictTransportSecurity: AbsoluteConfigKey = .init(.Security.strictTransportSecurity)
}
}
@@ -0,0 +1,95 @@
import Configuration
extension ConfigKey {
/// A namespace for the analytics configuration keys.
public enum Analytics {
/// The configuration key for the analytics website identifier (cleared to disable analytics).
public static let websiteID: ConfigKey = "analytics.websiteID"
/// The configuration key for the comma-delimited domains the tracker reports from.
public static let domains: ConfigKey = "analytics.domains"
/// The configuration key for recorder mode, loading the session recorder script alongside the tracker (set to `false` to disable).
public static let recorder: ConfigKey = "analytics.recorder"
}
/// A namespace for the static files cache configuration keys.
public enum Cache {
/// The configuration key for the max-age, in seconds, applied to fingerprinted assets (CSS, JavaScript) and fonts.
public static let maxAgeAsset: ConfigKey = "cache.maxAge.asset"
/// The configuration key for the max-age, in seconds, applied to unversioned text-based static files (e.g. plain text).
public static let maxAgeText: ConfigKey = "cache.maxAge.text"
/// The configuration key for the max-age, in seconds, applied to image static files (ICO, PNG, SVG).
public static let maxAgeImage: ConfigKey = "cache.maxAge.image"
/// The configuration key for the max-age, in seconds, applied to all other static files (e.g. the web manifest).
public static let maxAgeDefault: ConfigKey = "cache.maxAge.default"
}
/// A namespace for the response compression configuration keys.
public enum Compression {
/// The configuration key for the minimum response body size, in bytes, before compression is applied.
public static let minResponseSize: ConfigKey = "compression.minimumResponseSize"
}
/// A namespace for the persistence configuration keys.
public enum Database {
/// The configuration key selecting migrate-and-exit mode (run migrations, then exit) instead of serving.
public static let migrate: ConfigKey = "database.migrate"
/// The configuration key for the persistence driver (`inMemory` or `postgres`).
public static let driver: ConfigKey = "database.driver"
/// The configuration key for the PostgreSQL host.
public static let host: ConfigKey = "database.host"
/// The configuration key for the PostgreSQL port.
public static let port: ConfigKey = "database.port"
/// The configuration key for the database name.
public static let name: ConfigKey = "database.name"
/// The configuration key for the database username.
public static let username: ConfigKey = "database.username"
/// The configuration key for the database password.
public static let password: ConfigKey = "database.password"
/// The configuration key for the TLS posture used when connecting (`off`, `prefer`, or `require`).
public static let tls: ConfigKey = "database.tls"
/// The configuration key for the maximum pooled connections per event loop.
public static let poolMaxPerEventLoop: ConfigKey = "database.pool.maxPerEventLoop"
/// The configuration key for the longest wait, in seconds, for a pooled connection to become available.
public static let poolTimeout: ConfigKey = "database.pool.timeout"
}
/// A namespace for the HTTP server configuration keys.
public enum HTTP {
/// The configuration key for the host the server binds to.
public static let host: ConfigKey = "http.host"
/// The configuration key for the port the server listens on.
public static let port: ConfigKey = "http.port"
/// The configuration key for the server's name.
public static let serverName: ConfigKey = "http.serverName"
}
/// A namespace for the logging configuration keys.
public enum Log {
/// The configuration key for the minimum log level.
public static let level: ConfigKey = "log.level"
}
/// A namespace for the rate limit configuration keys.
public enum RateLimit {
/// The configuration key for the number of requests admitted per client per window.
public static let limit: ConfigKey = "rateLimit.limit"
/// The configuration key for the window length, in seconds.
public static let window: ConfigKey = "rateLimit.window"
/// The configuration key for keying clients by the first `X-Forwarded-For` entry (enable only behind a trusted proxy).
public static let trustForwardedFor: ConfigKey = "rateLimit.trustForwardedFor"
}
/// A namespace for the path configuration keys.
public enum Path {
/// The configuration key for the directory the static files are served from.
public static let staticFiles: ConfigKey = "path.staticFiles"
}
/// A namespace for the security headers configuration keys.
public enum Security {
/// The configuration key for the `Content-Security-Policy` header value.
public static let contentSecurityPolicy: ConfigKey = "security.contentSecurityPolicy"
/// The configuration key for the `X-Content-Type-Options` header value.
public static let contentTypeOptions: ConfigKey = "security.contentTypeOptions"
/// The configuration key for the `X-Frame-Options` header value.
public static let frameOptions: ConfigKey = "security.frameOptions"
/// The configuration key for the `Referrer-Policy` header value.
public static let referrerPolicy: ConfigKey = "security.referrerPolicy"
/// The configuration key for the `Permissions-Policy` header value.
public static let permissionsPolicy: ConfigKey = "security.permissionsPolicy"
/// The configuration key for the `Strict-Transport-Security` header value (omitted when unset).
public static let strictTransportSecurity: ConfigKey = "security.strictTransportSecurity"
}
}
@@ -0,0 +1,9 @@
import Configuration
extension ConfigValue {
/// A namespace for the HTTP server's default configuration values.
public enum HTTP {
/// The default server name.
public static let serverName: ConfigValue = .init(stringLiteral: .Server.name)
}
}
@@ -0,0 +1,27 @@
extension Int {
/// A namespace for the cache's default configuration values.
public enum Cache {
/// The default max-age, in seconds, applied to fingerprinted assets and fonts (1 year).
public static let maxAgeAsset = 31_536_000
/// The default max-age, in seconds, applied to unversioned text-based static files (1 hour).
public static let maxAgeText = 3_600
/// The default max-age, in seconds, applied to image static files (1 week).
public static let maxAgeImage = 604_800
/// The default max-age, in seconds, applied to all other static files (1 day).
public static let maxAgeDefault = 86_400
}
/// A namespace for the response compression's default configuration values.
public enum Compression {
/// The default minimum response body size, in bytes, before compression is applied (1 KB).
public static let minResponseSize = 1_024
}
/// A namespace for the persistence's default configuration values.
public enum Database {
/// The default PostgreSQL port.
public static let port = 5_432
/// The default maximum pooled connections per event loop.
public static let poolMaxPerEventLoop = 4
/// The default longest wait, in seconds, for a pooled connection to become available (the driver's own default).
public static let poolTimeout = 10
}
}
@@ -0,0 +1,13 @@
import Foundation
import Localization
public extension LanguageList {
// MARK: Initializers
/// Creates a language list backed by the module's String Catalog.
init() {
self.init(bundle: .module)
}
}
@@ -0,0 +1,13 @@
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)
}
}
@@ -0,0 +1,25 @@
import Foundation
import Infrastructure
public extension NotFoundMiddleware {
// MARK: Initializers
/// Creates a not-found middleware that renders the website's error page, localized to the module's String Catalog languages.
/// - Parameters:
/// - assetVersion: the version token appended to the page's asset URLs, or `nil` (the default) to leave them unversioned.
/// - analytics: the analytics tracker the error page embeds, or `nil` (the default) to omit it.
init(
assetVersion: String? = nil,
analytics: Analytics? = nil
) {
self.init(bundle: .module) {
NotFoundPage(
locale: $0,
assetVersion: assetVersion,
analytics: analytics
)
}
}
}
@@ -0,0 +1,53 @@
extension String {
/// A namespace for the analytics default configuration values.
///
/// Analytics ships **off**: ``websiteID`` is empty, so the pages embed no tracker until a deployment sets `analytics.websiteID`. Point
/// ``origin`` at your own instance before enabling it the placeholder is an [RFC 2606](https://www.rfc-editor.org/rfc/rfc2606)
/// reserved domain, so an unconfigured copy can never report to someone else's server.
public enum Analytics {
/// The origin the analytics scripts are loaded from and their beacons are sent to (scheme and host, no trailing slash).
///
/// Single-sourced here: both ``scriptURL`` and the session recorder script the pages embed in recorder mode derive from this
/// constant. It is deliberately not a configuration key the `Content-Security-Policy` must allow the same origin, and a value that
/// can drift at runtime would silently break the tracker it is supposed to permit.
public static let origin = "https://analytics.example.com"
/// The URL the analytics tracker script is loaded from.
public static let scriptURL = "\(origin)/script"
/// The default analytics website identifier the tracker reports as: empty, which omits the tracker entirely.
public static let websiteID = ""
/// The default comma-delimited domains the tracker reports from: empty, which reports from every host.
///
/// Once set, keep it paired with the host the pages are served at a deployment that serves from another host without matching
/// `analytics.domains` reports from a host it no longer serves, so analytics silently records nothing.
public static let domains = ""
}
/// A namespace for the persistence's default configuration values and recognized tokens.
public enum Database {
/// The default persistence driver: in-memory SQLite, which needs no external infrastructure.
public static let driver = "inMemory"
/// The driver token selecting the PostgreSQL backend.
public static let driverPostgres = "postgres"
/// The default PostgreSQL host.
public static let host = "localhost"
/// The default database name.
public static let name = "ccn"
/// The default database username.
public static let username = "ccn"
/// The default TLS posture token.
public static let tls = "prefer"
/// The TLS token disabling TLS.
public static let tlsOff = "off"
/// The TLS token requiring TLS.
public static let tlsRequire = "require"
}
/// A namespace for well-known path string constants.
public enum Path {
/// The directory, relative to the working directory, that the website's static files are served from.
public static let staticResources = "Resources/Static"
}
/// A namespace for the server string constants.
public enum Server {
/// The website server's name.
public static let name = "CCNWebsite"
}
}