Skip to content

chore(ui) Self-host web fonts (close PR-T1 GDPR follow-up)#172

Merged
jkeeley2073 merged 4 commits into
mainfrom
Dev-Phase5SelfHostFonts
May 10, 2026
Merged

chore(ui) Self-host web fonts (close PR-T1 GDPR follow-up)#172
jkeeley2073 merged 4 commits into
mainfrom
Dev-Phase5SelfHostFonts

Conversation

@jkeeley2073
Copy link
Copy Markdown
Contributor

@jkeeley2073 jkeeley2073 commented May 9, 2026

Summary

  • Removes the Google Fonts CDN dependency that PR-T1 feat(user-delight) Phase 5 PR-T1: Modern LCD token alignment #166 left in place. Twelve woff2 weights (Inter 400/500/600/700, Barlow Condensed 500/700, JetBrains Mono 400/500, Roboto 300/400/500/700) now ship as static web assets under src/PinballWizard.Web/wwwroot/fonts/, with @font-face + font-display: swap declarations in wwwroot/app.css.
  • Each family directory carries its upstream OFL 1.1 LICENSE.txt with the original copyright holder preserved (Inter Project Authors, Barlow Project Authors, JetBrains Mono Project Authors, Roboto Project Authors). wwwroot/fonts/README.md and a new "Third-party fonts" subsection in the project README document the provenance and attribution.
  • Regression test at tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs (5 facts) pins the conversion: no fonts.googleapis.com / fonts.gstatic.com URLs in App.razor or app.css, every expected family-weight pair has a @font-face block with font-display: swap, every referenced woff2 exists on disk with valid wOF2 magic bytes, every family directory has a LICENSE.txt mentioning the SIL OFL.

Why

PinballWizard is 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. Per CLAUDE.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

woff2 binaries are taken verbatim from the @fontsource v5.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 failed
  • SelfHostedFontsTests (new) — 5 facts pass and would fail if any @font-face block, woff2 file, or LICENSE.txt is missing or if a Google Fonts URL is re-introduced
  • StaticWebAssets manifest verified — 17 /fonts/... runtime entries (12 woff2 + 4 LICENSE.txt + 1 README.md) registered for serving from same-origin
  • Manual: ran the app and verified zero CDN font requests in a real browser.
    • performance.getEntriesByType("resource") → 0 hits to fonts.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 per font-display: swap.
    • document.fonts.statusloaded, document.fonts.size12 (all 12 @font-face rules parsed).
    • Computed body font-family = Inter, Roboto, sans-serif; computed heading font-family = "Barlow Condensed", Roboto, sans-serif — Modern LCD spec stack rendering from local woff2.
    • Console: 0 errors.

PR self-audit

  • Local review (qualitative, general-purpose agent): 0 🔴 / 2 ⚠️ / 11 ✅. Both ⚠️ findings were test-quality nits in the regression suite; the higher-value one (per-block font-display: swap assertion) is fixed in this PR. The other (test path resolution fallback) is the established project pattern and was deferred.
  • Mechanical 10-item self-audit (reviewer agent): 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

…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.
@jkeeley2073 jkeeley2073 added the claude-code Generated with Claude Code label May 9, 2026
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
Comment thread tests/PinballWizard.Web.Tests/StaticAssets/SelfHostedFontsTests.cs Fixed
[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")))
@jkeeley2073 jkeeley2073 merged commit 8ae69f4 into main May 10, 2026
5 checks passed
@jkeeley2073 jkeeley2073 deleted the Dev-Phase5SelfHostFonts branch May 10, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-code Generated with Claude Code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants