chore(ui) Self-host web fonts (close PR-T1 GDPR follow-up)#172
Merged
Conversation
…o, Roboto) Removes the Google Fonts CDN dependency from PinballWizard.Web and ships twelve woff2 weights as static web assets under wwwroot/fonts/, with @font-face declarations + font-display: swap in app.css. Why: PinballWizard is a customer-facing showcase / reference app. The LG Munchen I ruling (Az. 3 O 17493/20, January 2022) classified Google Fonts CDN as a GDPR violation because it leaks visitor IPs to Google on every page load. Per CLAUDE.md section Showcase obligations, the demo must not exhibit patterns a privacy-aware prospect would flag. PR-T1's local review (#166) flagged this as a critical finding and deferred the fix to this scoped task because self-hosting expands scope beyond a token-alignment recipe. Provenance: woff2 binaries taken verbatim from the @fontsource v5.2.5 npm packages on jsdelivr (which re-package the canonical upstream font files unmodified). Each family ships its upstream LICENSE.txt with the original copyright holder preserved (Inter Project Authors, Barlow Project Authors, JetBrains Mono Project Authors, Roboto Project Authors -- all OFL 1.1). Coverage: latin subset only, twelve weights matching the families and weights loaded by App.razor (Inter 400/500/600/700, Barlow Condensed 500/700, JetBrains Mono 400/500, Roboto 300/400/500/700). Tests: 1209 (Scraper) + 41 (Web) = 1250 passing, 0 warnings. Local review: 0 critical / 2 minor / 11 ok. Mechanical 10-item self-audit: clean. Identity verified personal-noreply.
| [Fact] | ||
| public void AppRazor_LoadsNoFontsFromGoogleCdn() | ||
| { | ||
| var appRazor = File.ReadAllText(Path.Combine(WebProjectRoot(), "Components", "App.razor")); |
| [Fact] | ||
| public void AppCss_LoadsNoFontsFromGoogleCdn() | ||
| { | ||
| var appCss = File.ReadAllText(Path.Combine(WebProjectRoot(), "wwwroot", "app.css")); |
| [Fact] | ||
| public void AppCss_DeclaresEveryExpectedFontFace() | ||
| { | ||
| var appCss = File.ReadAllText(Path.Combine(WebProjectRoot(), "wwwroot", "app.css")); |
| [Fact] | ||
| public void EveryReferencedFontFileExistsOnDisk() | ||
| { | ||
| var fontsRoot = Path.Combine(WebProjectRoot(), "wwwroot", "fonts"); |
|
|
||
| foreach (var (_, _, fileSlug, dirSlug) in ExpectedWeights) | ||
| { | ||
| var path = Path.Combine(fontsRoot, dirSlug, fileSlug); |
| [Fact] | ||
| public void EveryFontFamilyDirectoryHasLicenseFile() | ||
| { | ||
| var fontsRoot = Path.Combine(WebProjectRoot(), "wwwroot", "fonts"); |
|
|
||
| foreach (var dirSlug in ExpectedWeights.Select(w => w.DirSlug).Distinct(StringComparer.Ordinal)) | ||
| { | ||
| var licensePath = Path.Combine(fontsRoot, dirSlug, "LICENSE.txt"); |
| throw new InvalidOperationException( | ||
| "Could not locate repo root (no PinballWizard.slnx found walking up from test assembly)."); | ||
| } | ||
| return Path.Combine(dir.FullName, "src", "PinballWizard.Web"); |
| private static string WebProjectRoot() | ||
| { | ||
| var dir = new DirectoryInfo(AppContext.BaseDirectory); | ||
| while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "PinballWizard.slnx"))) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
woff2weights (Inter 400/500/600/700, Barlow Condensed 500/700, JetBrains Mono 400/500, Roboto 300/400/500/700) now ship as static web assets undersrc/PinballWizard.Web/wwwroot/fonts/, with@font-face+font-display: swapdeclarations inwwwroot/app.css.LICENSE.txtwith the original copyright holder preserved (Inter Project Authors, Barlow Project Authors, JetBrains Mono Project Authors, Roboto Project Authors).wwwroot/fonts/README.mdand a new "Third-party fonts" subsection in the project README document the provenance and attribution.tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs(5 facts) pins the conversion: nofonts.googleapis.com/fonts.gstatic.comURLs inApp.razororapp.css, every expected family-weight pair has a@font-faceblock withfont-display: swap, every referencedwoff2exists on disk with validwOF2magic bytes, every family directory has aLICENSE.txtmentioning the SIL OFL.Why
PinballWizardis a customer-facing showcase / reference app for Earlybird Solutions. The German court ruling LG München I, Az. 3 O 17493/20 (January 2022) classified Google Fonts CDN as a GDPR violation because it leaks visitor IP addresses to Google on every page load. PerCLAUDE.md§ "Showcase obligations", the demo must not exhibit patterns a privacy-aware prospect in a regulated industry would flag — self-hosting closes that hole.PR-T1 #166's local review flagged this as 🔴 #1 and deferred the fix to this scoped task because self-hosting expands scope (~16 binary asset files + per-family attribution) beyond the token-alignment recipe.
Provenance
woff2binaries are taken verbatim from the@fontsourcev5.2.5 npm packages on the jsdelivr CDN. Fontsource re-packages the canonical Google Fonts / upstream font files without modification. Latin subset only.Test plan
dotnet build PinballWizard.slnx— clean (0 warnings, 0 errors)dotnet test PinballWizard.slnx— 1316 passing (1256 Scraper + 60 Web), 0 failedSelfHostedFontsTests(new) — 5 facts pass and would fail if any@font-faceblock,woff2file, orLICENSE.txtis missing or if a Google Fonts URL is re-introduced/fonts/...runtime entries (12woff2+ 4LICENSE.txt+ 1README.md) registered for serving from same-originperformance.getEntriesByType("resource")→ 0 hits tofonts.googleapis.com/fonts.gstatic.com/ any external host; 5 active same-origin woff2 fetches (Inter 400/500, Barlow Condensed 500/700, Roboto 500); other 7 weights lazy-loaded on demand perfont-display: swap.document.fonts.status→loaded,document.fonts.size→12(all 12@font-facerules parsed).font-family=Inter, Roboto, sans-serif; computed headingfont-family="Barlow Condensed", Roboto, sans-serif— Modern LCD spec stack rendering from local woff2.PR self-audit
general-purposeagent): 0 🔴 / 2font-display: swapassertion) is fixed in this PR. The other (test path resolution fallback) is the established project pattern and was deferred.revieweragent): clean. Items 1–7 either clean or N/A for a static-asset PR. Items 8 (Cosmos surface) and 9 (User-Delight surface) not applicable. Identity verified personal-noreply.🤖 Generated with Claude Code