From 7aee1cd949fd02e5eb1cc7bdb57a5157598c587f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 25 Aug 2026 16:59:07 +0200 Subject: [PATCH] Added immutable caching to the fingerprinted media types for the ConfigReader+Properties extension in the Website service target. --- .../Extensions/ConfigReader+Properties.swift | 16 ++++++-- Services/Website/Tests/App/AppTests.swift | 8 +++- .../App/ConfigReaderPropertiesTests.swift | 38 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/Services/Website/Sources/App/Extensions/ConfigReader+Properties.swift b/Services/Website/Sources/App/Extensions/ConfigReader+Properties.swift index f0b7399..d3acee0 100644 --- a/Services/Website/Sources/App/Extensions/ConfigReader+Properties.swift +++ b/Services/Website/Sources/App/Extensions/ConfigReader+Properties.swift @@ -56,10 +56,15 @@ package extension ConfigReader { /// The `Cache-Control` policy applied to static files, grouped by media type. /// /// The max-ages are read from the `cache.maxAge.asset`, `cache.maxAge.text`, `cache.maxAge.image`, and - /// `cache.maxAge.default` keys. Stylesheets and scripts are referenced through fingerprinted URLs (see `FingerprintAssets`) and - /// fonts are immutable subset files, so all three are served long-lived and `immutable` — a deploy busts them by changing the URL, never by - /// revalidation. The remaining text files (e.g. `robots.txt`) keep their unversioned URLs and require revalidation once stale; images and - /// everything else are served public with their max-age alone. The groups match in order, so the specific types precede the `text` category. + /// `cache.maxAge.default` keys. Stylesheets, scripts, videos, JPEGs, and WebPs are referenced through fingerprinted URLs (see + /// `FingerprintAssets`) and fonts are immutable subset files, so all of them are served long-lived and `immutable` — a deploy busts them by + /// changing the URL, never by revalidation. The remaining text files (e.g. `robots.txt`) keep their unversioned URLs and require revalidation + /// once stale; the remaining images cannot be `immutable` — the group covers the icons, and a browser fetches `/favicon.ico` unversioned + /// whatever the markup says — and everything else is served public with its max-age alone. The groups match in order, so the specific types + /// precede the `text` and `image` categories. + /// + /// - Important: an image the markup references without a `?v=` token must be in neither JPEG nor WebP, or it is served immutable for a year and + /// a deploy cannot dislodge it. var cacheControl: CacheControl { let maxAgeAsset = int( forKey: .Cache.maxAgeAsset, @@ -82,6 +87,9 @@ package extension ConfigReader { (.textCss, [.public, .maxAge(maxAgeAsset), .immutable]), (.textJavascript, [.public, .maxAge(maxAgeAsset), .immutable]), (.font, [.public, .maxAge(maxAgeAsset), .immutable]), + (.videoMp4, [.public, .maxAge(maxAgeAsset), .immutable]), + (.imageJpeg, [.public, .maxAge(maxAgeAsset), .immutable]), + (.imageWebp, [.public, .maxAge(maxAgeAsset), .immutable]), (.text, [.public, .maxAge(maxAgeText), .mustRevalidate]), (.image, [.public, .maxAge(maxAgeImage)]), (.init(type: .any), [.public, .maxAge(maxAgeDefault)]), diff --git a/Services/Website/Tests/App/AppTests.swift b/Services/Website/Tests/App/AppTests.swift index 372d6d5..37676d2 100644 --- a/Services/Website/Tests/App/AppTests.swift +++ b/Services/Website/Tests/App/AppTests.swift @@ -14,10 +14,14 @@ struct AppTests { // MARK: Constants - // Stylesheets and scripts are referenced through fingerprinted URLs, so they are served immutable. + // Referenced through fingerprinted URLs, or an immutable subset file in the case of a font, so all of these are served immutable. private let immutableExtensions: [AssetExtension] = [ .css, - .js + .jpg, + .js, + .mp4, + .webp, + .woff2 ] // Absolute path to the copy of the package's "Resources/Static" folder made into the test bundle diff --git a/Services/Website/Tests/App/ConfigReaderPropertiesTests.swift b/Services/Website/Tests/App/ConfigReaderPropertiesTests.swift index 3558d39..0f89b72 100644 --- a/Services/Website/Tests/App/ConfigReaderPropertiesTests.swift +++ b/Services/Website/Tests/App/ConfigReaderPropertiesTests.swift @@ -61,6 +61,44 @@ struct ConfigReaderPropertiesTests { #expect(reader(values: [.Analytics.websiteID: ""]).analytics == nil) } + @Test(arguments: [ + "shared.css", + "shared.js", + "milker-400.woff2", + "booth.mp4", + "portrait.jpg", + "portrait.webp" + ]) + func `cache control to mark the fingerprinted media immutable`( + file: String + ) throws { + let header = try #require(reader().cacheControl.getCacheControlHeader(for: file)) + + #expect(header.contains("immutable")) + } + + @Test(arguments: [ + // The icons are exempt: a browser fetches `/favicon.ico` unversioned whatever the markup says, and the manifest names the PNGs. + "favicon.ico", + "icon-192.png", + "icon.svg" + ]) + func `cache control to leave the unversioned images mutable`( + file: String + ) throws { + let header = try #require(reader().cacheControl.getCacheControlHeader(for: file)) + + #expect(!header.contains("immutable")) + } + + @Test + func `cache control to have the unversioned text revalidate`() throws { + let header = try #require(reader().cacheControl.getCacheControlHeader(for: "robots.txt")) + + #expect(header.contains("must-revalidate")) + #expect(!header.contains("immutable")) + } + } // MARK: - Helpers