Project updates from Template (#1)
This PR contains the latest updates from the generic Website template, which have been added while working on #loud-amsterdam. Reviewed-on: #1 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/// A rendition an image asset is available at, from the narrowest up to the full-size file.
|
||||
///
|
||||
/// ``StaticFile`` names one file per rendition, and a page's `srcset` offers them all so the browser picks by rendered width.
|
||||
/// The declaration order is the `srcset` order: narrowest first.
|
||||
enum ImageWidth: CaseIterable {
|
||||
/// The small rendition, 480 pixels wide.
|
||||
case small
|
||||
/// The medium rendition, 800 pixels wide.
|
||||
case medium
|
||||
/// The large rendition: the full-size file, 1200 pixels wide.
|
||||
case large
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
extension ImageWidth {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The rendition's width in pixels: what the file is resampled to, and what its `srcset` descriptor states.
|
||||
var width: Int {
|
||||
switch self {
|
||||
case .small: 480
|
||||
case .medium: 800
|
||||
case .large: 1200
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,24 @@ import Localization
|
||||
/// The site-wide defaults shared by every page of the website.
|
||||
extension Page {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// The fonts preloaded on every page: one per face the stylesheets actually render.
|
||||
///
|
||||
/// Listed rather than derived from every WOFF2 in ``StaticFile/all``: a subset gated by a `unicode-range` no page reaches would add a
|
||||
/// download that never otherwise happens. Empty until the site ships fonts of its own.
|
||||
static var preloadedFonts: [StaticFile] {
|
||||
[]
|
||||
}
|
||||
|
||||
/// The territories paired with the site's languages, in Open Graph's `language_TERRITORY` form.
|
||||
///
|
||||
/// English ships as `en_US`, Open Graph's conventional default. A site serving a language in a territory of its own repoints or extends the
|
||||
/// map (`en_NL`, `nl_NL`, …); a language with no entry stays a bare code, which scrapers also accept.
|
||||
static var ogLocales: [String: String] {
|
||||
["en": "en_US"]
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The document language, derived from the page's locale and falling back to the default language.
|
||||
@@ -14,9 +32,83 @@ extension Page {
|
||||
?? LanguageList().default
|
||||
}
|
||||
|
||||
/// The icon, manifest, and theme colour metadata shared by every page of the website.
|
||||
@HTMLBuilder
|
||||
/// The locale of the page's social card, in Open Graph's `language_TERRITORY` form where ``ogLocales`` pairs the page's language with a
|
||||
/// territory; a language it does not name stays a bare code, which scrapers also accept.
|
||||
///
|
||||
/// Keyed off ``lang``, so the card's locale and the document's `lang` attribute never describe the document differently. Nothing reads it
|
||||
/// until a page supplies a `socialCard` — like ``preloadedFonts``, it is a hook a generated site fills in.
|
||||
var ogLocale: String {
|
||||
Self.ogLocales[lang] ?? lang
|
||||
}
|
||||
|
||||
/// The site-wide head metadata; a page that adds tags of its own composes ``siteMetadata`` rather than replacing it.
|
||||
var metadata: some HTML {
|
||||
siteMetadata
|
||||
}
|
||||
|
||||
/// The `hreflang` alternates tying the page's language editions together, or nothing without an origin — the annotations require absolute URLs.
|
||||
///
|
||||
/// Nothing is emitted for a single-language site either: a set naming one edition tells a search engine nothing it cannot already see.
|
||||
/// `x-default` points at the catalog's default language, whose bare URL negotiates the language and so is the right landing for everyone
|
||||
/// unmatched — it stays the catalog's default even when `languages` names a subset.
|
||||
/// - Parameters:
|
||||
/// - origin: the site's public origin, or `nil` to emit nothing.
|
||||
/// - path: the page's bare (default-language) path.
|
||||
/// - languages: the languages the page is published in; every catalog language by default. A page translated into only some of them
|
||||
/// narrows the set, so it never advertises an edition that does not exist.
|
||||
/// - Returns: one `alternate` link per language, followed by the `x-default` link.
|
||||
@HTMLBuilder
|
||||
func languageAlternates(
|
||||
origin: String?,
|
||||
path: String,
|
||||
languages: [Language] = Language.all
|
||||
) -> some HTML {
|
||||
if let origin, languages.count > 1 {
|
||||
ForEach(languages) { language in
|
||||
link(
|
||||
.rel("alternate"),
|
||||
.custom(
|
||||
name: "hreflang",
|
||||
value: language.identifier
|
||||
),
|
||||
.href(language.url(
|
||||
origin: origin,
|
||||
path: path
|
||||
))
|
||||
)
|
||||
}
|
||||
link(
|
||||
.rel("alternate"),
|
||||
.custom(
|
||||
name: "hreflang",
|
||||
value: "x-default"
|
||||
),
|
||||
.href(Language.default.url(
|
||||
origin: origin,
|
||||
path: path
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ``preloadedFonts`` links, then the icon, manifest, and theme colour metadata shared by every page of the website.
|
||||
///
|
||||
/// A preload URL must match the stylesheet's `@font-face` source exactly — unversioned, with `crossorigin` — or the browser fetches the
|
||||
/// font twice.
|
||||
@HTMLBuilder
|
||||
var siteMetadata: some HTML {
|
||||
for file in Self.preloadedFonts {
|
||||
link(
|
||||
.rel("preload"),
|
||||
.href(file.urlPath(for: .woff2)),
|
||||
.as(.font),
|
||||
.custom(
|
||||
name: "type",
|
||||
value: AssetExtension.woff2.contentType
|
||||
),
|
||||
.crossorigin(.anonymous)
|
||||
)
|
||||
}
|
||||
link(
|
||||
.rel(.icon),
|
||||
.href(StaticFile.favicon.urlPath(
|
||||
|
||||
@@ -17,6 +17,9 @@ struct IndexPage {
|
||||
/// The locale the page content is localized to.
|
||||
let locale: Locale
|
||||
|
||||
/// The public origin the page derives its canonical URL and language alternates from, or `nil` to omit them.
|
||||
let siteOrigin: String?
|
||||
|
||||
/// Resolves the page's text from the bundled String Catalog for the page's ``locale``.
|
||||
private let localize: Localize
|
||||
|
||||
@@ -26,15 +29,18 @@ struct IndexPage {
|
||||
/// - 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.
|
||||
/// - 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 embedded in the document head, or `nil` (the default) to omit it.
|
||||
init(
|
||||
locale: Locale,
|
||||
assetVersion: String? = nil,
|
||||
siteOrigin: String? = nil,
|
||||
analytics: Analytics? = nil
|
||||
) {
|
||||
self.analytics = analytics
|
||||
self.assetVersion = assetVersion
|
||||
self.locale = locale
|
||||
self.siteOrigin = siteOrigin
|
||||
self.localize = .init(bundle: .module)
|
||||
}
|
||||
|
||||
@@ -44,6 +50,36 @@ struct IndexPage {
|
||||
|
||||
extension IndexPage: Page {
|
||||
|
||||
// MARK: Constants
|
||||
|
||||
/// The path the page is served at; ``RootController`` registers its route against it, and the page builds its canonical URL from it.
|
||||
static let path = "/"
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// The canonical URL of the page's edition in its language, or `nil` when the origin is unknown.
|
||||
///
|
||||
/// The origin alone for the default language, since canonical URLs carry no trailing slash — `TrailingSlashRedirectMiddleware` enforces
|
||||
/// that on every path but the root, which has nothing to strip.
|
||||
var canonicalURL: String? {
|
||||
siteOrigin.map {
|
||||
Language(of: locale).url(
|
||||
origin: $0,
|
||||
path: Self.path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The site-wide metadata, preceded by the `hreflang` alternates tying the page's language editions together.
|
||||
@HTMLBuilder
|
||||
var metadata: some HTML {
|
||||
languageAlternates(
|
||||
origin: siteOrigin,
|
||||
path: Self.path
|
||||
)
|
||||
siteMetadata
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var content: some HTML {
|
||||
|
||||
@@ -25,8 +25,7 @@ struct NotFoundPage {
|
||||
/// 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.
|
||||
/// - 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,
|
||||
@@ -38,9 +37,10 @@ struct NotFoundPage {
|
||||
self.locale = locale
|
||||
self.localize = .init(bundle: .module)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Page
|
||||
// MARK: - Page
|
||||
|
||||
extension NotFoundPage: Page {
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import Foundation
|
||||
import Localization
|
||||
|
||||
/// A language the website serves, as its String Catalog names it.
|
||||
///
|
||||
/// The catalog stays the single source of truth: a locale appears in ``all`` once it has a localization, with no code change. The default
|
||||
/// language owns the site's bare paths; every other language answers under a prefix of its own, so each edition has a stable URL a search
|
||||
/// engine can index and the pages' `hreflang` alternates have somewhere to point.
|
||||
struct Language: Hashable, Sendable {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The language's identifier, as the String Catalog names it (e.g. `en`, `nl`).
|
||||
let identifier: String
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates the language with the given identifier, whether or not the catalog serves it.
|
||||
///
|
||||
/// Unvalidated so the URL rules can be exercised — and a language pinned — without the catalog shipping that localization first.
|
||||
/// - Parameter identifier: the language's identifier.
|
||||
init(identifier: String) {
|
||||
self.identifier = identifier
|
||||
}
|
||||
|
||||
/// Creates the language a locale reads as, falling back to ``default`` for anything the catalog does not serve.
|
||||
/// - Parameter locale: the locale a page is localized to.
|
||||
init(of locale: Locale) {
|
||||
guard
|
||||
let identifier = locale.language.languageCode?.identifier,
|
||||
Self.all.contains(Language(identifier: identifier))
|
||||
else {
|
||||
self = .default
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.identifier = identifier
|
||||
}
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// Whether the language owns the site's unprefixed paths.
|
||||
var isDefault: Bool {
|
||||
self == .default
|
||||
}
|
||||
|
||||
/// The prefix of the language's URLs: empty for the default language, which owns the bare paths.
|
||||
var pathPrefix: String {
|
||||
isDefault ? "" : "/\(identifier)"
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// The language's URL path for a page's bare path; the root collapses onto the prefix alone, so a Dutch home is `/nl`, not `/nl/`.
|
||||
/// - Parameter barePath: the page's unprefixed path, as ``RootController`` registers it for the default language.
|
||||
/// - Returns: the path this language's edition of the page is served at.
|
||||
func path(_ barePath: String) -> String {
|
||||
guard barePath == "/" else {
|
||||
return pathPrefix + barePath
|
||||
}
|
||||
|
||||
return pathPrefix.isEmpty ? barePath : pathPrefix
|
||||
}
|
||||
|
||||
/// The absolute URL of the page's edition in this language; the default language's root stays the bare origin, keeping canonicals slashless.
|
||||
/// - Parameters:
|
||||
/// - origin: the site's public origin, without a trailing slash.
|
||||
/// - barePath: the page's unprefixed path.
|
||||
/// - Returns: the absolute URL this language's edition of the page is served at.
|
||||
func url(
|
||||
origin: String,
|
||||
path barePath: String
|
||||
) -> String {
|
||||
let path = path(barePath)
|
||||
|
||||
return path == "/" ? origin : origin + path
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Catalog
|
||||
|
||||
extension Language {
|
||||
|
||||
// MARK: Computed
|
||||
|
||||
/// Every language the module's String Catalog provides a localization for, in the catalog list's stable order.
|
||||
static var all: [Language] {
|
||||
LanguageList()
|
||||
.all
|
||||
.map(Language.init(identifier:))
|
||||
}
|
||||
|
||||
/// The language served when no supported language matches a request: the catalog's source language.
|
||||
static var `default`: Language {
|
||||
.init(identifier: LanguageList().default)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import Infrastructure
|
||||
|
||||
/// A static file shipped with the website service.
|
||||
///
|
||||
/// Each constant declares one file stored under the static files root (the `Resources/Static` directory) and served by Hummingbird's
|
||||
/// `FileMiddleware` middleware. A file can be available with more than one extension (see ``fileExtensions``), each resolving to its own file, and
|
||||
/// sits in its extension's own folder unless it names a ``folder`` of its own.
|
||||
struct StaticFile: Asset {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The file extensions the file is available with.
|
||||
let fileExtensions: [AssetExtension]
|
||||
|
||||
/// The file's name, without extension.
|
||||
let fileName: String
|
||||
|
||||
/// The folder holding the file, or `nil` when it sits in its extensions' own folders.
|
||||
///
|
||||
/// Imagery sits in a folder per page — `img/index`, `img/about` — rather than in its extension's own.
|
||||
let folder: String?
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Declares a static file.
|
||||
/// - Parameters:
|
||||
/// - fileName: the file's name, without extension.
|
||||
/// - folder: the folder holding the file, or `nil` (the default) to use each extension's own folder.
|
||||
/// - fileExtensions: the extensions the file is available with, one file each.
|
||||
init(
|
||||
_ fileName: String,
|
||||
in folder: String? = nil,
|
||||
as fileExtensions: AssetExtension...
|
||||
) {
|
||||
self.fileExtensions = fileExtensions
|
||||
self.fileName = fileName
|
||||
self.folder = folder
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
extension StaticFile {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
/// Every file the service ships.
|
||||
///
|
||||
/// Spelled out because Swift cannot enumerate a type's own constants: a file missing here is still served, but goes untested.
|
||||
static let all: [Self] = [
|
||||
.appleTouchIcon,
|
||||
.favicon,
|
||||
.icon,
|
||||
.icon192,
|
||||
.icon512,
|
||||
.index,
|
||||
.notFound,
|
||||
.robots,
|
||||
.shared,
|
||||
.site,
|
||||
.sitemap,
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
extension StaticFile {
|
||||
|
||||
/// The `srcset` value offering every rendition of a responsive image, narrowest first, ending on the full-size file.
|
||||
///
|
||||
/// The renditions are named by the given closure, which conventionally gives the `large` one the bare file name and the narrower ones their
|
||||
/// width — as `srcset` files are usually named:
|
||||
///
|
||||
/// ```swift
|
||||
/// static func portrait(_ width: ImageWidth) -> Self {
|
||||
/// Self(width == .large ? "portrait" : "portrait-\(width.width)", in: "img/about", as: .jpg, .webp)
|
||||
/// }
|
||||
/// ```
|
||||
/// - Parameters:
|
||||
/// - fileExtension: the format the renditions are named in.
|
||||
/// - version: the version token appended to each URL, or `nil` (the default) to leave them unversioned.
|
||||
/// - rendition: the file naming a given width.
|
||||
/// - Returns: the `srcset` value for that format.
|
||||
static func srcSet(
|
||||
for fileExtension: AssetExtension,
|
||||
version: String? = nil,
|
||||
rendition: (ImageWidth) -> Self
|
||||
) -> String {
|
||||
ImageWidth.allCases
|
||||
.map { "\(rendition($0).urlPath(for: fileExtension, version: version)) \($0.width)w" }
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
extension StaticFile {
|
||||
/// The `apple-touch-icon.png` icon.
|
||||
static let appleTouchIcon = Self("apple-touch-icon", as: .png)
|
||||
|
||||
/// The `favicon.ico` icon.
|
||||
static let favicon = Self("favicon", as: .ico)
|
||||
|
||||
/// The `icon.svg` icon.
|
||||
static let icon = Self("icon", as: .svg)
|
||||
|
||||
/// The `icon-192.png` icon for the web manifest.
|
||||
static let icon192 = Self("icon-192", as: .png)
|
||||
|
||||
/// The `icon-512.png` icon for the web manifest.
|
||||
static let icon512 = Self("icon-512", as: .png)
|
||||
|
||||
/// The `css/index.css` stylesheet and `js/index.js` script for the landing page.
|
||||
static let index = Self("index", as: .css, .js)
|
||||
|
||||
/// The `css/not-found.css` stylesheet and `js/not-found.js` script for the not-found page.
|
||||
static let notFound = Self("not-found", as: .css, .js)
|
||||
|
||||
/// The `robots.txt` crawler directives.
|
||||
static let robots = Self("robots", as: .txt)
|
||||
|
||||
/// The `css/shared.css` stylesheet and `js/shared.js` script shared across pages.
|
||||
static let shared = Self("shared", as: .css, .js)
|
||||
|
||||
/// The `site.webmanifest` web application manifest.
|
||||
static let site = Self("site", as: .webmanifest)
|
||||
|
||||
/// The `sitemap.xml` crawler sitemap.
|
||||
static let sitemap = Self("sitemap", as: .xml)
|
||||
}
|
||||
@@ -116,6 +116,9 @@ private extension HealthController {
|
||||
}
|
||||
|
||||
/// Builds a JSON response carrying the given status and payload.
|
||||
///
|
||||
/// Every response is marked `noindex`: the checks answer `200 OK` to anyone, and `robots.txt` allows the whole site. A `Disallow` rule would
|
||||
/// stop the crawl but not the indexing, and would publish the paths to everyone reading the file.
|
||||
/// - Parameters:
|
||||
/// - status: the HTTP status of the response.
|
||||
/// - payload: the JSON body of the response.
|
||||
@@ -126,7 +129,10 @@ private extension HealthController {
|
||||
) -> Response {
|
||||
Response(
|
||||
status: status,
|
||||
headers: [.contentType: "application/json"],
|
||||
headers: [
|
||||
.contentType: "application/json",
|
||||
.robotsTag: "noindex",
|
||||
],
|
||||
body: .init(byteBuffer: .init(string: payload))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,15 +25,18 @@ public struct RootController<Context: LocalizedRequestContext> {
|
||||
/// 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.responses = .init(bundle: .module) {
|
||||
IndexPage(
|
||||
locale: $0,
|
||||
assetVersion: assetVersion,
|
||||
siteOrigin: siteOrigin,
|
||||
analytics: analytics
|
||||
)
|
||||
}
|
||||
@@ -55,6 +58,15 @@ extension RootController: RouterController {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -84,6 +96,23 @@ private extension RootController {
|
||||
)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// - 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
|
||||
@@ -91,7 +120,7 @@ private extension RootController {
|
||||
private extension RouterPath {
|
||||
/// A namespace for the ``RootController`` route paths.
|
||||
enum Root {
|
||||
/// The path of the landing page.
|
||||
static let index: RouterPath = "/"
|
||||
/// The path of the landing page; the page builds its canonical URL from the same constant.
|
||||
static let index: RouterPath = .init(IndexPage.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,11 @@ extension AbsoluteConfigKey {
|
||||
/// The absolute configuration key for the server's name.
|
||||
public static let serverName: AbsoluteConfigKey = .init(.HTTP.serverName)
|
||||
}
|
||||
/// A namespace for the HTTPS redirect configuration keys, as absolute keys.
|
||||
public enum HTTPS {
|
||||
/// The absolute configuration key for reading the visitor's original scheme from the `X-Forwarded-Proto` header.
|
||||
public static let trustForwardedProto: AbsoluteConfigKey = .init(.HTTPS.trustForwardedProto)
|
||||
}
|
||||
/// A namespace for the logging configuration keys, as absolute keys.
|
||||
public enum Log {
|
||||
/// The absolute configuration key for the minimum log level.
|
||||
|
||||
@@ -58,6 +58,12 @@ extension ConfigKey {
|
||||
/// The configuration key for the server's name.
|
||||
public static let serverName: ConfigKey = "http.serverName"
|
||||
}
|
||||
/// A namespace for the HTTPS redirect configuration keys.
|
||||
public enum HTTPS {
|
||||
/// The configuration key for reading the visitor's original scheme from the `X-Forwarded-Proto` header, redirecting the plain-HTTP
|
||||
/// ones to the site origin (enable only behind a trusted proxy that sets the header).
|
||||
public static let trustForwardedProto: ConfigKey = "https.trustForwardedProto"
|
||||
}
|
||||
/// A namespace for the logging configuration keys.
|
||||
public enum Log {
|
||||
/// The configuration key for the minimum log level.
|
||||
@@ -92,4 +98,9 @@ extension ConfigKey {
|
||||
/// The configuration key for the `Strict-Transport-Security` header value (omitted when unset).
|
||||
public static let strictTransportSecurity: ConfigKey = "security.strictTransportSecurity"
|
||||
}
|
||||
/// A namespace for the site configuration keys.
|
||||
public enum Site {
|
||||
/// The configuration key for the public origin the site is served at (scheme and host, no trailing slash).
|
||||
public static let origin: ConfigKey = "site.origin"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@ 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.
|
||||
/// Creates a not-found middleware that renders the website's not-found 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.
|
||||
/// - analytics: the analytics tracker the page embeds, or `nil` (the default) to omit the tracker script.
|
||||
init(
|
||||
assetVersion: String? = nil,
|
||||
analytics: Analytics? = nil
|
||||
|
||||
@@ -24,7 +24,9 @@ extension String {
|
||||
/// 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"
|
||||
public static let driver = driverInMemory
|
||||
/// The driver token selecting the in-memory SQLite backend.
|
||||
public static let driverInMemory = "inMemory"
|
||||
/// The driver token selecting the PostgreSQL backend.
|
||||
public static let driverPostgres = "postgres"
|
||||
/// The default PostgreSQL host.
|
||||
@@ -34,9 +36,11 @@ extension String {
|
||||
/// The default database username.
|
||||
public static let username = "ccn"
|
||||
/// The default TLS posture token.
|
||||
public static let tls = "prefer"
|
||||
public static let tls = tlsPrefer
|
||||
/// The TLS token disabling TLS.
|
||||
public static let tlsOff = "off"
|
||||
/// The TLS token upgrading to TLS only when the server offers it.
|
||||
public static let tlsPrefer = "prefer"
|
||||
/// The TLS token requiring TLS.
|
||||
public static let tlsRequire = "require"
|
||||
}
|
||||
@@ -50,4 +54,13 @@ extension String {
|
||||
/// The website server's name.
|
||||
public static let name = "CCNWebsite"
|
||||
}
|
||||
/// A namespace for the site string constants.
|
||||
public enum Site {
|
||||
/// The default public origin the site is served at (scheme and host, no trailing slash).
|
||||
///
|
||||
/// Bootstrap writes the canonical URL it prompts for here, leaving it empty for the placeholder. An empty or non-HTTPS origin disables
|
||||
/// the HTTPS redirect, which `https.trustForwardedProto` must enable besides — a `301` is cached for a long time, so it is never issued
|
||||
/// at a host nobody named.
|
||||
public static let origin = "https://ccn.rock-n-co.de"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user