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:
2026-09-04 13:40:35 +00:00
committed by javier
parent 08b4d80064
commit 65b62681eb
60 changed files with 2347 additions and 326 deletions
@@ -0,0 +1,24 @@
import Testing
@testable import WebsiteLibrary
@Suite(
"ImageWidth enumeration",
.tags(.enumeration)
)
struct ImageWidthTests {
// MARK: Computed tests
@Test(arguments: zip(
ImageWidth.allCases,
[480, 800, 1200]
))
func `width`(
for imageWidth: ImageWidth,
expects width: Int
) {
#expect(imageWidth.width == width)
}
}
@@ -1,82 +0,0 @@
import Infrastructure
import Testing
@testable import WebsiteLibrary
@Suite(
"StaticFile enumeration",
.tags(.enumeration)
)
struct StaticFileTests {
// MARK: Type aliases
typealias File = StaticFile
// MARK: Computed tests
@Test(arguments: zip(
File.allCases,
Self.fileExtensions
))
func `file extensions`(
for file: File,
expects extensions: [AssetExtension]
) {
#expect(file.fileExtensions == extensions)
}
@Test(arguments: zip(
File.allCases,
Self.fileNames
))
func `file name`(
for file: File,
expects fileName: String
) {
#expect(file.fileName == fileName)
}
// MARK: CaseIterable tests
@Test
func `all cases`() {
#expect(File.allCases.count == 11)
}
}
// MARK: - Helpers
private extension StaticFileTests {
// MARK: Constants
static let fileExtensions: [[AssetExtension]] = [
[.png],
[.ico],
[.svg],
[.png],
[.png],
[.css, .js],
[.css, .js],
[.txt],
[.css, .js],
[.webmanifest],
[.xml]
]
static let fileNames: [String] = [
"apple-touch-icon",
"favicon",
"icon",
"icon-192",
"icon-512",
"index",
"not-found",
"robots",
"shared",
"site",
"sitemap"
]
}
@@ -0,0 +1,128 @@
import Elementary
import Foundation
import Infrastructure
import Testing
@testable import WebsiteLibrary
@Suite(
"Page+Defaults extension",
.tags(.extensionTests)
)
struct PageDefaultsTests {
// MARK: Computed tests
@Test
func `pairs english with a territory for its social card locale`() {
#expect(StubPage().ogLocale == "en_US")
}
/// Open Graph prefers `language_TERRITORY`, but a scraper accepts the bare code better than pinning a territory the site never named.
@Test
func `leaves an unpaired language as a bare code`() {
#expect(StubPage(locale: .init(identifier: "nl")).ogLocale == "nl")
}
// MARK: Method tests
@Test
func `renders no language alternates without an origin`() {
let html = StubPage().languageAlternates(
origin: nil,
path: "/",
languages: Self.languages
).render()
#expect(html.isEmpty)
}
@Test
func `renders no language alternates for a single language`() {
let html = StubPage().languageAlternates(
origin: "https://example.com",
path: "/",
languages: [.default]
).render()
#expect(html.isEmpty)
}
/// The default language keeps the bare root; the prefixed edition collapses onto its prefix rather than carrying a trailing slash.
@Test
func `renders an alternate per language and an x-default at the root`() {
let html = StubPage().languageAlternates(
origin: "https://example.com",
path: "/",
languages: Self.languages
).render()
#expect(html.contains(#"<link rel="alternate" hreflang="en" href="https://example.com">"#))
#expect(html.contains(#"<link rel="alternate" hreflang="nl" href="https://example.com/nl">"#))
#expect(html.contains(#"<link rel="alternate" hreflang="x-default" href="https://example.com">"#))
}
@Test
func `prefixes the alternates of a nested page`() {
let html = StubPage().languageAlternates(
origin: "https://example.com",
path: "/privacy",
languages: Self.languages
).render()
#expect(html.contains(#"<link rel="alternate" hreflang="en" href="https://example.com/privacy">"#))
#expect(html.contains(#"<link rel="alternate" hreflang="nl" href="https://example.com/nl/privacy">"#))
#expect(html.contains(#"<link rel="alternate" hreflang="x-default" href="https://example.com/privacy">"#))
}
}
// MARK: - Helpers
private extension PageDefaultsTests {
// MARK: Constants
/// A two-language set, standing in for the multi-language catalog the template itself does not ship.
static let languages: [Language] = [
.default,
.init(identifier: "nl")
]
// MARK: Types
/// The barest ``Page`` conformance, so the shared defaults can be exercised without a real page's content.
struct StubPage: Page {
// MARK: Properties
let assetVersion: String? = nil
let locale: Locale
// MARK: Initializers
init(locale: Locale = .init(identifier: "en")) {
self.locale = locale
}
// MARK: Properties
var content: some HTML {
HTMLRaw("")
}
var scripts: [any Asset] {
[]
}
var stylesheets: [any Asset] {
[]
}
var title: String {
"Stub"
}
}
}
@@ -32,6 +32,39 @@ struct IndexPageTests {
#expect(html.contains("/js/index.js"))
}
@Test
func `renders no font preloads until fonts are declared`() {
// The template ships no fonts, so a preload link here would point at a file the service does not serve.
#expect(IndexPage.preloadedFonts.isEmpty)
#expect(!IndexPage(locale: .init(identifier: "en")).render().contains(#"rel="preload""#))
}
@Test
func `renders no canonical URL until an origin is given`() {
#expect(!IndexPage(locale: .init(identifier: "en")).render().contains(#"rel="canonical""#))
}
@Test
func `renders the canonical URL as the bare origin for the default language`() {
// The root is the one path with no trailing slash for `TrailingSlashRedirectMiddleware` to strip.
let html = IndexPage(
locale: .init(identifier: "en"),
siteOrigin: "https://example.com"
).render()
#expect(html.contains(#"<link rel="canonical" href="https://example.com">"#))
}
@Test
func `renders no language alternates for a single-language catalog`() {
// The template serves one language, so a set naming one edition would tell a crawler nothing.
#expect(Language.all.count == 1)
#expect(!IndexPage(
locale: .init(identifier: "en"),
siteOrigin: "https://example.com"
).render().contains("hreflang"))
}
@Test
func `renders versioned asset URLs when given a version`() {
let html = IndexPage(
@@ -0,0 +1,109 @@
import Foundation
import Testing
@testable import WebsiteLibrary
@Suite(
"Language type",
.tags(.type)
)
struct LanguageTests {
// MARK: Catalog tests
/// The template ships an English-only catalog, so a site adding a language sees this pin fail and reviews the URL rules below.
@Test
func `derives its languages from the String Catalog`() {
#expect(Language.all == [Language(identifier: "en")])
#expect(Language.default == Language(identifier: "en"))
}
// MARK: Initializer tests
@Test
func `reads the language a locale names`() {
#expect(Language(of: .init(identifier: "en")).identifier == "en")
}
@Test
func `reads a regional locale as its primary language`() {
#expect(Language(of: .init(identifier: "en_GB")).identifier == "en")
}
@Test
func `falls back to the default for a language the catalog does not serve`() {
#expect(Language(of: .init(identifier: "fr")) == .default)
}
// MARK: Computed tests
@Test
func `owns the bare paths as the default language`() {
let language = Language.default
#expect(language.isDefault)
#expect(language.pathPrefix.isEmpty)
}
@Test
func `prefixes its paths as a non-default language`() {
let language = Language(identifier: "nl")
#expect(!language.isDefault)
#expect(language.pathPrefix == "/nl")
}
// MARK: Method tests
@Test(arguments: zip(
["/", "/privacy"],
["/", "/privacy"]
))
func `path`(
forDefaultLanguageAt barePath: String,
expects path: String
) {
#expect(Language.default.path(barePath) == path)
}
/// The root collapses onto the prefix alone: a trailing slash is the very spelling `TrailingSlashRedirectMiddleware` redirects away from.
@Test(arguments: zip(
["/", "/privacy"],
["/nl", "/nl/privacy"]
))
func `path`(
forPrefixedLanguageAt barePath: String,
expects path: String
) {
#expect(Language(identifier: "nl").path(barePath) == path)
}
@Test(arguments: zip(
["/", "/privacy"],
["https://example.com", "https://example.com/privacy"]
))
func `url`(
forDefaultLanguageAt barePath: String,
expects url: String
) {
#expect(Language.default.url(
origin: "https://example.com",
path: barePath
) == url)
}
@Test(arguments: zip(
["/", "/privacy"],
["https://example.com/nl", "https://example.com/nl/privacy"]
))
func `url`(
forPrefixedLanguageAt barePath: String,
expects url: String
) {
#expect(Language(identifier: "nl").url(
origin: "https://example.com",
path: barePath
) == url)
}
}
@@ -0,0 +1,109 @@
import Infrastructure
import Testing
@testable import WebsiteLibrary
@Suite(
"StaticFile type",
.tags(.type)
)
struct StaticFileTests {
// MARK: Type aliases
typealias File = StaticFile
// MARK: Computed tests
/// The path is asserted rather than the three facts behind it: it is the only form the rest of the service sees.
@Test(arguments: Self.paths)
func `relative paths`(
for file: File,
expects paths: [String]
) {
#expect(file.fileExtensions.map(file.relativePath) == paths)
}
/// A preload URL must match the stylesheet's `@font-face` source exactly, so it carries the extension's folder and no version query.
@Test
func `unversioned font paths`() {
let font = File("a-font-400", as: .woff2)
#expect(font.relativePath(for: .woff2) == "font/a-font-400.woff2")
#expect(font.urlPath(for: .woff2) == "/font/a-font-400.woff2")
}
/// Imagery sits in a folder per page, so a declared folder replaces the extension's own for every extension the file has.
@Test
func `declared folder paths`() {
let portrait = File("portrait", in: "img/about", as: .jpg, .webp)
#expect(portrait.relativePath(for: .jpg) == "img/about/portrait.jpg")
#expect(portrait.relativePath(for: .webp) == "img/about/portrait.webp")
}
// MARK: Methods tests
@Test(arguments: zip(
[AssetExtension.jpg, .webp],
["jpg", "webp"]
))
func `srcset offers every rendition narrowest first`(
for fileExtension: AssetExtension,
expects suffix: String
) {
let srcSet = File.srcSet(for: fileExtension) { width in
File(
width == .large ? "portrait" : "portrait-\(width.width)",
in: "img/about",
as: .jpg, .webp
)
}
#expect(srcSet == [
"/img/about/portrait-480.\(suffix) 480w",
"/img/about/portrait-800.\(suffix) 800w",
"/img/about/portrait.\(suffix) 1200w"
].joined(separator: ", "))
}
@Test
func `srcset versions every rendition when given a version`() {
let srcSet = File.srcSet(for: .jpg, version: "0123456789abcdef") { _ in
File("portrait", in: "img/about", as: .jpg)
}
#expect(srcSet.components(separatedBy: "?v=0123456789abcdef").count - 1 == ImageWidth.allCases.count)
}
// MARK: Constants tests
@Test
func `all files`() {
#expect(File.all.count == Self.paths.count)
}
}
// MARK: - Helpers
private extension StaticFileTests {
// MARK: Constants
/// Every file paired with the path it is served at, one per extension, in ``StaticFile/all`` order.
static let paths: [(File, [String])] = [
(.appleTouchIcon, ["apple-touch-icon.png"]),
(.favicon, ["favicon.ico"]),
(.icon, ["icon.svg"]),
(.icon192, ["icon-192.png"]),
(.icon512, ["icon-512.png"]),
(.index, ["css/index.css", "js/index.js"]),
(.notFound, ["css/not-found.css", "js/not-found.js"]),
(.robots, ["robots.txt"]),
(.shared, ["css/shared.css", "js/shared.js"]),
(.site, ["site.webmanifest"]),
(.sitemap, ["sitemap.xml"]),
]
}
@@ -1,5 +1,6 @@
import Hummingbird
import HummingbirdTesting
import Infrastructure
import Logging
import NIOCore
import Persistence
@@ -29,6 +30,7 @@ struct HealthControllerTests {
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/json")
#expect(body == #"{"status":"ok"}"#)
#expect(response.headers[.robotsTag] == "noindex")
}
}
}
@@ -54,6 +56,7 @@ struct HealthControllerTests {
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/json")
#expect(body == #"{"status":"ready"}"#)
#expect(response.headers[.robotsTag] == "noindex")
}
}
} catch {
@@ -99,6 +102,7 @@ struct HealthControllerTests {
#expect(response.status == .serviceUnavailable)
#expect(response.headers[.contentType] == "application/json")
#expect(body == #"{"status":"unavailable"}"#)
#expect(response.headers[.robotsTag] == "noindex")
}
}
} catch {
@@ -131,6 +131,37 @@ struct RootControllerTests {
}
}
@Test
func `serves the landing page with a canonical URL when an origin is configured`() async throws {
try await app(
siteOrigin: "https://example.com"
).test(.router) { client in
try await client.execute(
uri: "/",
method: .get
) { response in
#expect(String(buffer: response.body).contains(#"<link rel="canonical" href="https://example.com">"#))
}
}
}
/// The template's catalog serves one language, so the default owns every path and no prefixed route is registered.
@Test
func `registers no prefixed route for a single-language catalog`() async throws {
let prefixed = Language.all.filter { !$0.isDefault }
#expect(prefixed.isEmpty)
try await app.test(.router) { client in
try await client.execute(
uri: "/en",
method: .get
) { response in
#expect(response.status == .notFound)
}
}
}
@Test
func `embeds no analytics tracker by default`() async throws {
try await app.test(.router) { client in
@@ -176,13 +207,15 @@ private extension RootControllerTests {
// MARK: Methods
/// Builds an application whose root controller appends the given version token to the landing
/// page's asset URLs and embeds the given analytics tracker.
/// page's asset URLs, derives its absolute links from the given origin, and embeds the given analytics tracker.
/// - Parameters:
/// - assetVersion: the version token appended to the page's asset URLs.
/// - 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.
/// - Returns: the configured application.
func app(
assetVersion: String? = nil,
siteOrigin: String? = nil,
analytics: Analytics? = nil
) -> some ApplicationProtocol {
let router = Router(context: WebsiteRequestContext.self)
@@ -193,6 +226,7 @@ private extension RootControllerTests {
router.addRoutes(RootController<WebsiteRequestContext>(
assetVersion: assetVersion,
siteOrigin: siteOrigin,
analytics: analytics
).routes)