Project updates from Template

This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing:

- Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305)
- Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df)
- Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9)
- Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769)
- Enabled the Analytics trait in the Website service. (f6a6bbd)
- Reworked page routing in the Website service. (d1cb2c1)
- Gated the structured data features in the Infrastructure package behind a trait. (f0a8791)
- Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3)
- Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b)

The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead.

The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
javierandClaude Opus 5.5 committed 2026-10-25 11:55:43 +01:00
1 parent 326299fc35
commit 911424de24
57 files changed
+864 -273

No files matched your search

@@ -1,3 +1,4 @@
#if Analytics
import Elementary
import Testing
@@ -57,3 +58,4 @@ struct AnalyticsEventTaggingTests {
}
}
#endif
@@ -0,0 +1,68 @@
#if Analytics
import Elementary
import Testing
@testable import Infrastructure
@Suite(
"MarkupContent+Reporting extension",
.tags(.extension)
)
struct MarkupContentReportingTests {
// MARK: Methods tests
@Test
func `renders the same markup as applying the event's tagging`() {
let event = Analytics.Event(
name: "playlist",
properties: ["set": "avc-xi"]
)
let reported = a(.href("/")) { "Listen" }
.reporting(event)
.render()
let tagged = a(.href("/")) { "Listen" }
.attributes(contentsOf: event.tagging())
.render()
#expect(reported == tagged)
#expect(reported == #"<a href="/" data-umami-event="playlist" data-umami-event-set="avc-xi">Listen</a>"#)
}
@Test
func `keeps the attributes chained before it`() {
let rendered = a(.href("https://example.com")) { "Listen" }
.attributes(.target(.blank), when: true)
.reporting(.init(name: "playlist"))
.render()
#expect(rendered == #"<a href="https://example.com" target="_blank" data-umami-event="playlist">Listen</a>"#)
}
@Test
func `tags a view under its own prefix`() {
let rendered = h2 { "KLM" }
.reporting(
.init(
name: "case_view",
properties: ["case": "klm"]
),
on: .view
)
.render()
#expect(rendered == #"<h2 data-view-event="case_view" data-view-event-case="klm">KLM</h2>"#)
}
@Test
func `reports from an SVG tag`() {
let rendered = SVG.path {}
.reporting(.init(name: "logo"))
.render()
#expect(rendered.contains(#"data-umami-event="logo""#))
}
}
#endif
@@ -101,6 +101,7 @@ struct PageTests {
#expect(html.contains(#"<meta name="twitter:card" content="summary_large_image">"#))
}
#if StructuredData
@Test
func `omits the structured data script by default`() {
let html = StubPage().render()
@@ -111,21 +112,25 @@ struct PageTests {
@Test
func `renders the structured data script when provided`() {
let html = StubPage(structuredData: .init(
name: "Stub Site",
url: "https://stub.example/",
logo: "https://stub.example/logo.png",
profiles: ["https://social.example/stub"]
organization: .init(
name: "Stub Site",
url: "https://stub.example/",
logo: "https://stub.example/logo.png",
profiles: ["https://social.example/stub"]
)
)).render()
#expect(html.contains(
#"<script type="application/ld+json">"# +
#"{"@context":"https://schema.org","@graph":["# +
#"{"@type":"Organization","@id":"https://stub.example/#organization","name":"Stub Site","url":"https://stub.example/","logo":"https://stub.example/logo.png","sameAs":["https://social.example/stub"]},"# +
#"{"@type":"WebSite","name":"Stub Site","url":"https://stub.example/","publisher":{"@id":"https://stub.example/#organization"}}]}"# +
#"{"@type":"WebSite","@id":"https://stub.example/#website","name":"Stub Site","url":"https://stub.example/","publisher":{"@id":"https://stub.example/#organization"}}]}"# +
#"</script>"#
))
}
#endif
#if Analytics
@Test
func `omits the analytics tracker by default`() {
let html = StubPage().render()
@@ -194,6 +199,7 @@ struct PageTests {
#expect(!html.contains("data-do-not-track"))
#expect(!html.contains("data-performance"))
}
#endif
@Test
func `appends the version token to the asset URLs`() {
@@ -1,3 +1,4 @@
#if Analytics
import Testing
@testable import Infrastructure
@@ -35,6 +36,21 @@ struct AnalyticsEventTests {
))
}
/// The tracker ignores `data-view-event`, so a view is never recorded as a click.
@Test
func `renders a view under its own prefix`() {
let event = Analytics.Event(
name: "case_view",
properties: ["case": "klm"]
)
#expect(event.attributes(on: .view) == [
.init("data-view-event", value: "case_view"),
.init("data-view-event-case", value: "klm"),
])
#expect(event.attributes == event.attributes(on: .click))
}
@Test
func `renders the name first, then the properties in the order given`() {
let event = Analytics.Event(
@@ -103,3 +119,4 @@ struct AnalyticsEventTests {
}
}
#endif
@@ -1,3 +1,4 @@
#if Analytics
import Testing
@testable import Infrastructure
@@ -133,3 +134,4 @@ struct AnalyticsTests {
}
}
#endif
@@ -40,6 +40,7 @@ struct SocialCardTests {
.init("2400", name: .imageWidth),
.init("1260", name: .imageHeight),
.init("An image.", name: .imageAlt),
.init("An image.", name: .twitterImageAlt),
.init("summary_large_image", name: .twitter),
])
}
@@ -70,6 +71,7 @@ struct SocialCardTests {
#expect(names.contains(.image))
#expect(!names.contains(.imageAlt))
#expect(!names.contains(.twitterImageAlt))
}
@Test
@@ -87,6 +89,7 @@ struct SocialCardTests {
@Test
func `keys a tag by the attribute its name dictates`() {
#expect(SocialCard.Tag.Name.twitter.attribute == .name)
#expect(SocialCard.Tag.Name.twitterImageAlt.attribute == .name)
#expect(SocialCard.Tag.Name.title.attribute == .property)
#expect(SocialCard.Tag("website", name: .type).attribute == .property)
}
@@ -1,3 +1,4 @@
#if StructuredData
import Foundation
import Testing
@@ -14,46 +15,80 @@ struct StructuredDataTests {
@Test
func `derives the full payload from complete data`() {
let data = StructuredData(
name: "A Site",
url: "https://site.example/",
logo: "https://site.example/logo.png",
inLanguage: ["en", "nl"],
profiles: [
"https://social.example/a-site",
"https://videos.example/a-site",
]
organization: .init(
name: "A Site",
url: "https://site.example/",
logo: "https://site.example/logo.png",
profiles: [
"https://social.example/a-site",
"https://videos.example/a-site",
]
),
inLanguage: ["en", "nl"]
)
#expect(data.payload == #"{"@context":"https://schema.org","@graph":["# +
#"{"@type":"Organization","@id":"https://site.example/#organization","name":"A Site","url":"https://site.example/","logo":"https://site.example/logo.png","# +
#""sameAs":["https://social.example/a-site","https://videos.example/a-site"]},"# +
#"{"@type":"WebSite","name":"A Site","url":"https://site.example/","publisher":{"@id":"https://site.example/#organization"},"inLanguage":["en","nl"]}]}"#
#"{"@type":"WebSite","@id":"https://site.example/#website","name":"A Site","url":"https://site.example/","publisher":{"@id":"https://site.example/#organization"},"inLanguage":["en","nl"]}]}"#
)
}
@Test
func `carries the organization's alternate name, description, area, and address in that order`() {
func `carries the organization's optional facts in order`() {
let data = StructuredData(
name: "A Site",
url: "https://site.example/",
alternateName: "The Site",
description: "What the site is.",
areaServed: "A City",
email: "hello@site.example"
organization: .init(
name: "A Site",
url: "https://site.example/",
alternateName: "The Site",
legalName: "A Site B.V.",
description: "What the site is.",
areaServed: "A City",
email: "hello@site.example",
telephone: "+31000000000",
address: .init(
street: "A Street 1",
postalCode: "1000 AA",
locality: "A City",
country: "NL"
),
vatID: "NL000000000B01",
iso6523Code: "0106:00000000",
foundingDate: "2000"
)
)
#expect(data.payload.contains(
#""name":"A Site","url":"https://site.example/","alternateName":"The Site","description":"What the site is.","areaServed":"A City","email":"hello@site.example""#
#""name":"A Site","url":"https://site.example/","alternateName":"The Site","legalName":"A Site B.V.","description":"What the site is.","# +
#""areaServed":"A City","email":"hello@site.example","telephone":"+31000000000","# +
#""address":{"@type":"PostalAddress","streetAddress":"A Street 1","postalCode":"1000 AA","addressLocality":"A City","addressCountry":"NL"},"# +
#""vatID":"NL000000000B01","iso6523Code":"0106:00000000","foundingDate":"2000"}"#
))
}
@Test
func `omits the optional facts when the data carries none`() {
let payload = StructuredData(
organization: .init(
name: "A Site",
url: "https://site.example/"
)
).payload
for name in ["legalName", "telephone", "address", "vatID", "iso6523Code", "foundingDate"] {
#expect(!payload.contains(name))
}
}
@Test
func `references the organization's founder by identifier`() {
let data = StructuredData(
name: "A Site",
url: "https://site.example/",
profiles: ["https://social.example/a-site"],
founder: "https://site.example/who#person"
organization: .init(
name: "A Site",
url: "https://site.example/",
profiles: ["https://social.example/a-site"],
founder: "https://site.example/who#person"
)
)
// Last of the organization's properties, after the profiles.
@@ -65,8 +100,10 @@ struct StructuredDataTests {
@Test
func `omits the founder when the data carries none`() {
let data = StructuredData(
name: "A Site",
url: "https://site.example/"
organization: .init(
name: "A Site",
url: "https://site.example/"
)
)
#expect(!data.payload.contains("founder"))
@@ -76,8 +113,10 @@ struct StructuredDataTests {
func `derives the organization's identifier from the site URL`() {
let url = "https://site.example/"
let data = StructuredData(
name: "A Site",
url: url
organization: .init(
name: "A Site",
url: url
)
)
// Another page references the organization by this, so it must name the node the initializer actually builds.
@@ -85,16 +124,33 @@ struct StructuredDataTests {
#expect(data.payload.contains(##""@id":"\##(StructuredData.organizationID(forSiteURL: url))""##))
}
@Test
func `derives the website's identifier from the site URL`() {
let url = "https://site.example/"
let data = StructuredData(
organization: .init(
name: "A Site",
url: url
)
)
// A page's own node references the website by this, so it must name the node the initializer actually builds.
#expect(StructuredData.websiteID(forSiteURL: url) == "https://site.example/#website")
#expect(data.payload.contains(##""@type":"WebSite","@id":"\##(StructuredData.websiteID(forSiteURL: url))""##))
}
@Test
func `omits the properties of the facts minimal data does not carry`() {
let data = StructuredData(
name: "A Site",
url: "https://site.example/"
organization: .init(
name: "A Site",
url: "https://site.example/"
)
)
#expect(data.payload == #"{"@context":"https://schema.org","@graph":["# +
#"{"@type":"Organization","@id":"https://site.example/#organization","name":"A Site","url":"https://site.example/"},"# +
#"{"@type":"WebSite","name":"A Site","url":"https://site.example/","publisher":{"@id":"https://site.example/#organization"}}]}"#
#"{"@type":"WebSite","@id":"https://site.example/#website","name":"A Site","url":"https://site.example/","publisher":{"@id":"https://site.example/#organization"}}]}"#
)
}
@@ -125,8 +181,10 @@ struct StructuredDataTests {
@Test
func `escapes the values it embeds in the payload`() {
let data = StructuredData(
name: #"A "Quoted" \ Site"#,
url: "https://site.example/</script>"
organization: .init(
name: #"A "Quoted" \ Site"#,
url: "https://site.example/</script>"
)
)
#expect(data.payload.contains(#""name":"A \"Quoted\" \\ Site""#))
@@ -199,10 +257,12 @@ struct StructuredDataTests {
func `derives a payload that parses back to the facts it carries`() throws {
let name = "A \"Site\"\nwith \\ every <hazard>"
let data = StructuredData(
name: name,
url: "https://site.example/</script>",
logo: "https://site.example/logo.png",
profiles: ["https://social.example/a-site"]
organization: .init(
name: name,
url: "https://site.example/</script>",
logo: "https://site.example/logo.png",
profiles: ["https://social.example/a-site"]
)
)
let object = try JSONSerialization.jsonObject(with: Data(data.payload.utf8))
@@ -215,3 +275,4 @@ struct StructuredDataTests {
}
}
#endif