From fd5aa849243ddbd7f162e0a2b0824b9d1ba2286b Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 2 Aug 2026 02:41:46 +0200 Subject: [PATCH 01/26] Improved the PNG assets optimization on the Dockerfile in the Website service target to do it recursively. --- Services/Website/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Services/Website/Dockerfile b/Services/Website/Dockerfile index 0714bda..c1c6879 100644 --- a/Services/Website/Dockerfile +++ b/Services/Website/Dockerfile @@ -19,7 +19,7 @@ WORKDIR /static COPY ./Services/Website/Resources/Static . RUN esbuild --minify --allow-overwrite --outdir=css css/*.css \ && esbuild --minify --allow-overwrite --outdir=js js/*.js \ - && oxipng --opt max --strip safe *.png \ + && oxipng --opt max --strip safe --recursive . \ && svgo --recursive --folder . # Export stage: `docker build --target assets-export --output ` writes the minified static files to for From 5c070c2b1ab691129cab876c7ad1961fd10e712c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Aug 2026 09:22:40 +0000 Subject: [PATCH 02/26] Integrated Analytics into the Infrastructure package (#40) This PR contains the work done to define the `Analytics` type into the _Infrastructure_ package and also, to integrate this type into its `Page` protocol. Reviewed-on: https://repo.rock-n-code.com/rock-n-code/loud-amsterdam/pulls/40 Co-authored-by: Javier Cicchelli --- .../Sources/Public/Protocols/Page.swift | 32 +++++- .../Sources/Public/Types/Analytics.swift | 97 +++++++++++++++++++ .../Cases/Public/Protocols/PageTests.swift | 34 +++++++ .../Cases/Public/Types/AnalyticsTests.swift | 55 +++++++++++ .../Tests/Utils/Pages/StubPage.swift | 7 ++ 5 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 Packages/Infrastructure/Sources/Public/Types/Analytics.swift create mode 100644 Packages/Infrastructure/Tests/Cases/Public/Types/AnalyticsTests.swift diff --git a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift index 30c25e1..e925ac3 100644 --- a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift +++ b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift @@ -4,8 +4,8 @@ import Foundation /// A page of a website: an HTML document with the shared scaffolding assembled around the page's content. /// /// A conforming page supplies its locale, its title, the stylesheets and scripts it needs, its head metadata, and its content; the protocol assembles the -/// rest of the document around them: the viewport declaration, the summary, canonical, and social card tags, the structured data script, and the -/// metadata followed by the stylesheet links in the head, and the content followed by the script tags in the body. +/// rest of the document around them: the viewport declaration, the summary, canonical, and social card tags, the structured data script, the analytics +/// tracker script, and the metadata followed by the stylesheet links in the head, and the content followed by the script tags in the body. public protocol Page: HTMLDocument, Sendable { // MARK: Associated types @@ -18,6 +18,9 @@ public protocol Page: HTMLDocument, Sendable { // MARK: Properties + /// The analytics tracker embedded as a deferred script in the document head, or `nil` (the default) to omit it. + var analytics: Analytics? { get } + /// The version token appended to the page's asset URLs, or `nil` to leave them unversioned. var assetVersion: String? { get } @@ -77,14 +80,15 @@ public extension Page { } } - /// The viewport declaration, the ``summary``, ``canonicalURL``, and ``socialCard`` tags and the ``structuredData`` script - /// (when provided), and the ``metadata`` followed by the ``stylesheets`` links, placed in the document head. + /// The viewport declaration, the ``summary``, ``canonicalURL``, and ``socialCard`` tags, the ``structuredData`` script and the + /// ``analytics`` tracker script (when provided), and the ``metadata`` followed by the ``stylesheets`` links, placed in the document head. /// /// The charset declaration is omitted: Elementary's `HTMLDocument` scaffolding already emits `` before this markup, /// and HTML5 allows only one. /// /// The structured data is an inert data block — browsers never execute it, so a site's `Content-Security-Policy` does not apply to it — - /// that search engines read for the organization's name, logo, and profiles. + /// that search engines read for the organization's name, logo, and profiles. The analytics tracker, by contrast, is an executable script the + /// policy must allow, and it is `defer`red so it never delays the page render; each behavior flag renders its `data-` attribute only when enabled. @HTMLBuilder var head: some HTML { meta( @@ -127,6 +131,19 @@ public extension Page { } } + if let analytics { + script( + .defer, + .src(analytics.scriptURL) + ) {} + .attributes(contentsOf: analytics.attributes.map { + .custom( + name: $0.name, + value: $0.value + ) + }) + } + metadata for file in stylesheets { @@ -140,6 +157,11 @@ public extension Page { } } + /// The analytics tracker is omitted unless the page provides one. + var analytics: Analytics? { + nil + } + /// The social card is omitted unless the page provides one. var socialCard: SocialCard? { nil diff --git a/Packages/Infrastructure/Sources/Public/Types/Analytics.swift b/Packages/Infrastructure/Sources/Public/Types/Analytics.swift new file mode 100644 index 0000000..4df4997 --- /dev/null +++ b/Packages/Infrastructure/Sources/Public/Types/Analytics.swift @@ -0,0 +1,97 @@ +/// The analytics tracker a page embeds: where the script loads from, which site it reports as, which domains it reports from, and how it behaves. +/// +/// A page carries it as an optional value so the ``Page`` scaffolding renders the tracker's deferred `"#)) + } + + @Test + func `omits the analytics behavior flags that are disabled`() { + let html = StubPage(analytics: .init( + scriptURL: "https://analytics.example.com/script", + websiteID: "0000-website-id", + domains: "example.com", + excludeHash: true, + doNotTrack: false, + performance: false + )).render() + + #expect(html.contains(#""#)) + #expect(!html.contains("data-do-not-track")) + #expect(!html.contains("data-performance")) + } + @Test func `appends the version token to the asset URLs`() { let html = StubPage(assetVersion: "0123456789abcdef").render() diff --git a/Packages/Infrastructure/Tests/Cases/Public/Types/AnalyticsTests.swift b/Packages/Infrastructure/Tests/Cases/Public/Types/AnalyticsTests.swift new file mode 100644 index 0000000..1ee9a42 --- /dev/null +++ b/Packages/Infrastructure/Tests/Cases/Public/Types/AnalyticsTests.swift @@ -0,0 +1,55 @@ +import Testing + +@testable import Infrastructure + +@Suite( + "Analytics type", + .tags(.type) +) +struct AnalyticsTests { + + // MARK: Functional tests + + @Test + func `lists the website id and domains with every behavior flag on by default`() { + let analytics = Analytics( + scriptURL: "https://analytics.example.com/script", + websiteID: "id-123", + domains: "example.com" + ) + + #expect(analytics.attributes.map(\.name) == [ + "data-website-id", + "data-domains", + "data-exclude-hash", + "data-do-not-track", + "data-performance", + ]) + #expect(analytics.attributes.map(\.value) == [ + "id-123", + "example.com", + "true", + "true", + "true", + ]) + } + + @Test + func `omits the disabled behavior flags`() { + let analytics = Analytics( + scriptURL: "https://analytics.example.com/script", + websiteID: "id-123", + domains: "example.com", + excludeHash: true, + doNotTrack: false, + performance: false + ) + + #expect(analytics.attributes.map(\.name) == [ + "data-website-id", + "data-domains", + "data-exclude-hash", + ]) + } + +} diff --git a/Packages/Infrastructure/Tests/Utils/Pages/StubPage.swift b/Packages/Infrastructure/Tests/Utils/Pages/StubPage.swift index ffc755e..d719f1d 100644 --- a/Packages/Infrastructure/Tests/Utils/Pages/StubPage.swift +++ b/Packages/Infrastructure/Tests/Utils/Pages/StubPage.swift @@ -7,6 +7,9 @@ struct StubPage: Page { // MARK: Properties + /// The analytics tracker rendered 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? @@ -34,6 +37,8 @@ struct StubPage: Page { /// default) to leave them unversioned. /// - canonicalURL: the canonical URL rendered in the document head, or `nil` (the default) /// to omit it. + /// - analytics: the analytics tracker rendered as a deferred script in the document head, + /// or `nil` (the default) to omit it. /// - socialCard: the card rendered as link-preview tags in the document head, or `nil` /// (the default) to omit them. /// - structuredData: the structured data rendered as a JSON-LD script in the document @@ -44,10 +49,12 @@ struct StubPage: Page { locale: Locale = .init(identifier: "en"), assetVersion: String? = nil, canonicalURL: String? = nil, + analytics: Analytics? = nil, socialCard: SocialCard? = nil, structuredData: StructuredData? = nil, summary: String? = nil ) { + self.analytics = analytics self.assetVersion = assetVersion self.canonicalURL = canonicalURL self.locale = locale From c603421c28858701c137ef1032e17c69c573d82e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Aug 2026 12:06:47 +0200 Subject: [PATCH 03/26] Implemented the scripts' deferral on the Page protocol in the Infrastructure package. --- .../Sources/Public/Protocols/Page.swift | 31 ++++++++++++------- .../Cases/Public/Protocols/PageTests.swift | 12 ++++--- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift index e925ac3..5f71622 100644 --- a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift +++ b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift @@ -5,7 +5,7 @@ import Foundation /// /// A conforming page supplies its locale, its title, the stylesheets and scripts it needs, its head metadata, and its content; the protocol assembles the /// rest of the document around them: the viewport declaration, the summary, canonical, and social card tags, the structured data script, the analytics -/// tracker script, and the metadata followed by the stylesheet links in the head, and the content followed by the script tags in the body. +/// tracker script, and the metadata followed by the stylesheet links and the deferred script tags in the head, and the content as the body. public protocol Page: HTMLDocument, Sendable { // MARK: Associated types @@ -27,7 +27,7 @@ public protocol Page: HTMLDocument, Sendable { /// The canonical URL the page is served at, rendered as a `link rel="canonical"` tag in the document head, or `nil` (the default) to omit the tag. var canonicalURL: String? { get } - /// The page's markup, rendered before the ``scripts``. + /// The page's markup, rendered as the document body. @HTMLBuilder var content: Content { get } @@ -38,7 +38,10 @@ public protocol Page: HTMLDocument, Sendable { @HTMLBuilder var metadata: Metadata { get } - /// The scripts loaded at the end of the document body, in order. + /// The scripts loaded from the document head, in order. + /// + /// Rendered as `defer`red tags: the downloads start while the head is parsed, and the scripts still execute in order only after the + /// document is fully parsed — the same semantics end-of-body tags would give, minus the late download start. var scripts: [any Asset] { get } /// The card controlling the page's link previews, rendered as Open Graph and Twitter meta tags in the document head, or `nil` (the default) @@ -67,21 +70,15 @@ public extension Page { nil } - /// The page ``content`` followed by its ``scripts``. + /// The page ``content``; the ``scripts`` load deferred from the ``head``. @HTMLBuilder var body: some HTML { content - - for file in scripts { - script(.src(file.urlPath( - for: .js, - version: assetVersion - ))) {} - } } /// The viewport declaration, the ``summary``, ``canonicalURL``, and ``socialCard`` tags, the ``structuredData`` script and the - /// ``analytics`` tracker script (when provided), and the ``metadata`` followed by the ``stylesheets`` links, placed in the document head. + /// ``analytics`` tracker script (when provided), and the ``metadata`` followed by the ``stylesheets`` links and the deferred + /// ``scripts`` tags, placed in the document head. /// /// The charset declaration is omitted: Elementary's `HTMLDocument` scaffolding already emits `` before this markup, /// and HTML5 allows only one. @@ -155,6 +152,16 @@ public extension Page { )) ) } + + for file in scripts { + script( + .defer, + .src(file.urlPath( + for: .js, + version: assetVersion + )) + ) {} + } } /// The analytics tracker is omitted unless the page provides one. diff --git a/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift b/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift index 2540ec2..38787fa 100644 --- a/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift +++ b/Packages/Infrastructure/Tests/Cases/Public/Protocols/PageTests.swift @@ -21,7 +21,7 @@ struct PageTests { #expect(html.contains(#"name="viewport""#)) #expect(html.contains(#""#)) #expect(html.contains(#""#)) - #expect(html.contains(#""#)) + #expect(html.contains(#""#)) #expect(html.contains("Stub content")) } @@ -38,13 +38,17 @@ struct PageTests { } @Test - func `renders the scripts after the content`() throws { + func `renders the scripts deferred in the head, after the stylesheets`() throws { let html = StubPage().render() - let content = try #require(html.range(of: "Stub content")) + let stylesheet = try #require(html.range(of: "/css/stub.css")) let script = try #require(html.range(of: "/js/stub.js")) + let content = try #require(html.range(of: "Stub content")) - #expect(content.lowerBound < script.lowerBound) + // Deferred head scripts start downloading during head parsing but still execute, in order, only after the + // document is parsed — the semantics end-of-body tags gave, minus the late download start. + #expect(stylesheet.lowerBound < script.lowerBound) + #expect(script.lowerBound < content.lowerBound) } @Test From 16acdf0f3d992a18732842693547484d4106f2d5 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 4 Aug 2026 12:11:57 +0200 Subject: [PATCH 04/26] Implemented the analytics preconnect hint on the Page protocol in the Infrastructure package. --- .../Sources/Public/Protocols/Page.swift | 16 ++++++--- .../Sources/Public/Types/Analytics.swift | 20 +++++++++++ .../Cases/Public/Protocols/PageTests.swift | 9 ++++- .../Cases/Public/Types/AnalyticsTests.swift | 33 +++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift index 5f71622..952ea4c 100644 --- a/Packages/Infrastructure/Sources/Public/Protocols/Page.swift +++ b/Packages/Infrastructure/Sources/Public/Protocols/Page.swift @@ -76,9 +76,9 @@ public extension Page { content } - /// The viewport declaration, the ``summary``, ``canonicalURL``, and ``socialCard`` tags, the ``structuredData`` script and the - /// ``analytics`` tracker script (when provided), and the ``metadata`` followed by the ``stylesheets`` links and the deferred - /// ``scripts`` tags, placed in the document head. + /// The viewport declaration, the ``analytics`` origin preconnect hint, the ``summary``, ``canonicalURL``, and ``socialCard`` + /// tags, the ``structuredData`` script and the ``analytics`` tracker script (when provided), and the ``metadata`` followed by the + /// ``stylesheets`` links and the deferred ``scripts`` tags, placed in the document head. /// /// The charset declaration is omitted: Elementary's `HTMLDocument` scaffolding already emits `` before this markup, /// and HTML5 allows only one. @@ -92,7 +92,15 @@ public extension Page { .name(.viewport), .content("width=device-width, initial-scale=1") ) - + + // Rendered first so the cross-origin handshake starts before the parser reaches the tracker script tag. + if let origin = analytics?.origin { + link( + .rel("preconnect"), + .href(origin) + ) + } + if let summary { meta( .name(.description), diff --git a/Packages/Infrastructure/Sources/Public/Types/Analytics.swift b/Packages/Infrastructure/Sources/Public/Types/Analytics.swift index 4df4997..378df23 100644 --- a/Packages/Infrastructure/Sources/Public/Types/Analytics.swift +++ b/Packages/Infrastructure/Sources/Public/Types/Analytics.swift @@ -1,3 +1,5 @@ +import Foundation + /// The analytics tracker a page embeds: where the script loads from, which site it reports as, which domains it reports from, and how it behaves. /// /// A page carries it as an optional value so the ``Page`` scaffolding renders the tracker's deferred `"#)) + + // The preconnect hint warms the tracker origin's connection before the parser reaches the script tag. + let preconnect = try #require(html.range(of: #""#)) + let script = try #require(html.range(of: #""#)) + + let tracker = try #require(html.range(of: #"