feat: self-hosted WOFF2 fonts via base64 embedding (#88) #96

Open
Claude wants to merge 1 commits from ai/paper-dynasty-database#88 into main
Collaborator

Closes #88

Summary

Eliminates all external Google Fonts CDN requests during card rendering by embedding the WOFF2 font files as base64 data URIs.

Changes

  • storage/templates/style.html — Added 5 @font-face declarations at the top of the <style> block, each with an inline data:font/woff2;base64,... URI:
    • Open Sans 300, 400, 700
    • Source Sans 3 400, 700
  • storage/templates/player_card.html — Removed 3 <link> tags pointing to fonts.googleapis.com and fonts.gstatic.com

Acceptance Criteria

  • No fonts.googleapis.com references in either template
  • No external <link> tags in player_card.html
  • All 5 font variants present as @font-face declarations
  • Base64 WOFF2 data URIs in place

Tests

All 4 unit acceptance criteria verified programmatically before committing:

OK: player_card.html has no external font links
OK: style.html has 5 @font-face declarations
OK: all 5 font variants (family + weight) present
OK: base64 WOFF2 data URIs present

Visual diff (pixel-identical check) requires a manual render comparison.

Closes #88 ## Summary Eliminates all external Google Fonts CDN requests during card rendering by embedding the WOFF2 font files as base64 data URIs. ## Changes - **`storage/templates/style.html`** — Added 5 `@font-face` declarations at the top of the `<style>` block, each with an inline `data:font/woff2;base64,...` URI: - Open Sans 300, 400, 700 - Source Sans 3 400, 700 - **`storage/templates/player_card.html`** — Removed 3 `<link>` tags pointing to `fonts.googleapis.com` and `fonts.gstatic.com` ## Acceptance Criteria - [x] No `fonts.googleapis.com` references in either template - [x] No external `<link>` tags in `player_card.html` - [x] All 5 font variants present as `@font-face` declarations - [x] Base64 WOFF2 data URIs in place ## Tests All 4 unit acceptance criteria verified programmatically before committing: ``` OK: player_card.html has no external font links OK: style.html has 5 @font-face declarations OK: all 5 font variants (family + weight) present OK: base64 WOFF2 data URIs present ``` Visual diff (pixel-identical check) requires a manual render comparison.
Claude added 1 commit 2026-03-13 06:03:56 +00:00
Closes #88

- Add 5 @font-face declarations with base64-encoded WOFF2 data to style.html
  (Open Sans 300/400/700, Source Sans 3 400/700)
- Remove three Google Fonts CDN <link> tags from player_card.html
- Source WOFF2 files already present in storage/fonts/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude added the
ai-reviewing
label 2026-03-13 06:15:48 +00:00
cal requested changes 2026-03-13 13:16:41 +00:00
cal left a comment
Owner

AI Code Review

Files Reviewed

  • storage/templates/player_card.html (modified)
  • storage/templates/style.html (modified)

Findings

Correctness

Critical: Duplicate font data inflates template by ~163 KB unnecessarily.

Both embedded fonts are variable fonts with wght axes, confirmed via fonttools inspection of the WOFF2 binaries:

  • Open Sans: wght axis 300–800 (covers all three requested weights: 300, 400, 700)
  • Source Sans 3: wght axis 200–900 (covers all two requested weights: 400, 700)

Because they are variable fonts, a single @font-face block per family — with font-weight: 300 800 for Open Sans and font-weight: 200 900 for Source Sans 3 — is all that is needed. The browser maps the CSS font-weight value to the wght axis automatically.

Instead, the PR embeds:

  • Open Sans: 3 identical WOFF2 blobs (48,320 bytes each, md5 dcf31ebe) → 3 × 64,428 base64 chars
  • Source Sans 3: 2 identical WOFF2 blobs (28,740 bytes each, md5 30164609) → 2 × 38,320 base64 chars

Total base64 added: ~270 KB. Optimal would be ~103 KB (one declaration per family with a weight range). The duplication wastes ~163 KB of inline data that is parsed on every Playwright card render.

The corrected declarations would be:

@font-face {
    font-family: "Open Sans";
    font-weight: 300 800;
    src: url("data:font/woff2;base64,<single_open_sans_b64>") format("woff2");
}
@font-face {
    font-family: "Source Sans 3";
    font-weight: 200 900;
    src: url("data:font/woff2;base64,<single_source_sans_b64>") format("woff2");
}

storage/templates/style.html, lines 1–28 (new @font-face blocks): The three Open Sans blocks (weights 300, 400, 700) all embed byte-for-byte identical WOFF2 data. The two Source Sans 3 blocks (weights 400, 700) also embed byte-for-byte identical WOFF2 data.

Security

No issues found. The base64 data URIs are inert font data with no execution surface; there are no external requests left after this change.

Style & Conventions

The removal of the three Google Fonts <link> tags from player_card.html is clean and correct. The @font-face placement at the top of style.html is appropriate.

Suggestions

  • Using font-weight: 300 800 (range syntax) in a single @font-face per family is the idiomatic way to declare a variable font in CSS. It is also what Google Fonts itself emits when you request a wght axis range. This would cut the size of style.html by roughly two-thirds compared to this PR.
  • style.html is included in every card render via Jinja {% include %}, so the extra ~163 KB is re-parsed by Chromium on each screenshot. This will have a measurable impact on card generation throughput at scale.

Verdict: REQUEST_CHANGES

The implementation achieves the stated goal — all external font requests are eliminated and the fonts render correctly because the variable font wght axes cover all needed weights. However, the approach embeds the same WOFF2 binary three times for Open Sans and twice for Source Sans 3, adding ~163 KB of redundant base64 data to every card render. This should be fixed to use one @font-face block per font family with a weight range before merging.


Automated review by Claude PR Reviewer

## AI Code Review ### Files Reviewed - `storage/templates/player_card.html` (modified) - `storage/templates/style.html` (modified) ### Findings #### Correctness **Critical: Duplicate font data inflates template by ~163 KB unnecessarily.** Both embedded fonts are variable fonts with `wght` axes, confirmed via fonttools inspection of the WOFF2 binaries: - Open Sans: `wght` axis 300–800 (covers all three requested weights: 300, 400, 700) - Source Sans 3: `wght` axis 200–900 (covers all two requested weights: 400, 700) Because they are variable fonts, a single `@font-face` block per family — with `font-weight: 300 800` for Open Sans and `font-weight: 200 900` for Source Sans 3 — is all that is needed. The browser maps the CSS `font-weight` value to the `wght` axis automatically. Instead, the PR embeds: - Open Sans: **3 identical WOFF2 blobs** (48,320 bytes each, md5 `dcf31ebe`) → 3 × 64,428 base64 chars - Source Sans 3: **2 identical WOFF2 blobs** (28,740 bytes each, md5 `30164609`) → 2 × 38,320 base64 chars Total base64 added: **~270 KB**. Optimal would be **~103 KB** (one declaration per family with a weight range). The duplication wastes ~163 KB of inline data that is parsed on every Playwright card render. The corrected declarations would be: ```css @font-face { font-family: "Open Sans"; font-weight: 300 800; src: url("data:font/woff2;base64,<single_open_sans_b64>") format("woff2"); } @font-face { font-family: "Source Sans 3"; font-weight: 200 900; src: url("data:font/woff2;base64,<single_source_sans_b64>") format("woff2"); } ``` **`storage/templates/style.html`, lines 1–28** (new @font-face blocks): The three Open Sans blocks (weights 300, 400, 700) all embed byte-for-byte identical WOFF2 data. The two Source Sans 3 blocks (weights 400, 700) also embed byte-for-byte identical WOFF2 data. #### Security No issues found. The base64 data URIs are inert font data with no execution surface; there are no external requests left after this change. #### Style & Conventions The removal of the three Google Fonts `<link>` tags from `player_card.html` is clean and correct. The `@font-face` placement at the top of `style.html` is appropriate. #### Suggestions - Using `font-weight: 300 800` (range syntax) in a single `@font-face` per family is the idiomatic way to declare a variable font in CSS. It is also what Google Fonts itself emits when you request a `wght` axis range. This would cut the size of `style.html` by roughly two-thirds compared to this PR. - `style.html` is included in every card render via Jinja `{% include %}`, so the extra ~163 KB is re-parsed by Chromium on each screenshot. This will have a measurable impact on card generation throughput at scale. ### Verdict: REQUEST_CHANGES The implementation achieves the stated goal — all external font requests are eliminated and the fonts render correctly because the variable font `wght` axes cover all needed weights. However, the approach embeds the same WOFF2 binary three times for Open Sans and twice for Source Sans 3, adding ~163 KB of redundant base64 data to every card render. This should be fixed to use one `@font-face` block per font family with a weight range before merging. --- *Automated review by Claude PR Reviewer*
@ -2,0 +22,4 @@
@font-face {
font-family: "Source Sans 3";
font-weight: 700;
src: url("data:font/woff2;base64,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
Owner

Open Sans: three identical WOFF2 blobs (48,320 bytes each, md5 dcf31ebe) are embedded here for weights 300, 400, and 700. This font is a variable font with a wght axis of 300–800, so a single @font-face with font-weight: 300 800 is sufficient. Replace these three blocks with one.

Open Sans: three identical WOFF2 blobs (48,320 bytes each, md5 dcf31ebe) are embedded here for weights 300, 400, and 700. This font is a variable font with a wght axis of 300–800, so a single @font-face with `font-weight: 300 800` is sufficient. Replace these three blocks with one.
Owner

Source Sans 3: two identical WOFF2 blobs (28,740 bytes each, md5 30164609) are embedded here for weights 400 and 700. This font is a variable font with a wght axis of 200–900. Replace these two blocks with one @font-face using font-weight: 200 900.

Source Sans 3: two identical WOFF2 blobs (28,740 bytes each, md5 30164609) are embedded here for weights 400 and 700. This font is a variable font with a wght axis of 200–900. Replace these two blocks with one @font-face using `font-weight: 200 900`.
cal changed target branch from next-release to card-evolution 2026-03-17 20:55:49 +00:00
cal changed target branch from card-evolution to next-release 2026-03-17 20:56:36 +00:00
cal requested changes 2026-03-23 13:15:43 +00:00
cal left a comment
Owner

AI Code Review

Files Reviewed

  • storage/templates/player_card.html (modified)
  • storage/templates/style.html (modified)

Findings

Correctness — CRITICAL BUG

All font weight variants embed identical binary data.

Hashing the base64 payloads in style.html reveals:

Declaration MD5 of payload
Open Sans 300 c319fb108e28e5dec54276b666549e16
Open Sans 400 c319fb108e28e5dec54276b666549e16
Open Sans 700 c319fb108e28e5dec54276b666549e16
Source Sans 3 400 7ebe648623e3aff6502dacdcaf774a27
Source Sans 3 700 7ebe648623e3aff6502dacdcaf774a27

The three @font-face blocks for Open Sans (weights 300, 400, 700) all embed the exact same WOFF2 binary. The two blocks for Source Sans 3 (weights 400, 700) are also identical to each other.

This means:

  • font-weight: 300 (light text) and font-weight: 700 (bold text) will both render using whichever single weight was accidentally embedded. Card visual quality is degraded — bold headings will not actually be bold.
  • The same bug exists in the source font files at storage/fonts/: open-sans-300.woff2, open-sans-400.woff2, and open-sans-700.woff2 are all the same file (MD5 dcf31ebe107435bd68e0164d59e19b87). The font files themselves are incorrect, so this is not a base64 encoding error in the template — the wrong source files were used.

The fix requires:

  1. Downloading the correct WOFF2 binaries for each weight variant. Open Sans weight files differ substantially in size and glyph set. The correct files should be sourced from Google Fonts or the official Open Sans/Source Sans 3 releases.
  2. Re-generating the base64 embeddings from the corrected WOFF2 files.
  3. Replacing all five src: lines in style.html with the correct payloads.

Security

  • No issues found. Removing the Google Fonts CDN links is the right move — it eliminates an external dependency and any associated privacy/availability concerns for Playwright rendering.

Style & Conventions

  • The @font-face declarations follow valid CSS syntax and include the correct format("woff2") specifier.
  • Removing the three <link> tags from player_card.html is clean and correct.
  • The PR description's acceptance criteria checklist is technically satisfied (5 declarations, base64 URIs present, no CDN links) but does not verify that each weight's payload is distinct — that gap allowed this bug through.

Suggestions

  • After sourcing the correct WOFF2 files, add a test that hashes (or at minimum checks the file size of) each embedded font blob to assert they are distinct. Something as simple as asserting that the five base64 strings are all different would have caught this.
  • Consider committing the WOFF2 source files to storage/fonts/ in git (if file size allows) so the embedded data can be audited and regenerated deterministically.

Verdict: REQUEST_CHANGES

The goal of this PR (eliminating CDN dependencies) is correct and the approach is sound. However, all three Open Sans weight variants are encoded from an identical source file, as are both Source Sans 3 variants. Card rendering will silently use a single font weight for all bold/light/regular text. The correct WOFF2 files must be sourced and the base64 payloads regenerated before this can merge.


Automated review by Claude PR Reviewer

## AI Code Review ### Files Reviewed - `storage/templates/player_card.html` (modified) - `storage/templates/style.html` (modified) ### Findings #### Correctness — CRITICAL BUG **All font weight variants embed identical binary data.** Hashing the base64 payloads in `style.html` reveals: | Declaration | MD5 of payload | |---|---| | Open Sans 300 | `c319fb108e28e5dec54276b666549e16` | | Open Sans 400 | `c319fb108e28e5dec54276b666549e16` | | Open Sans 700 | `c319fb108e28e5dec54276b666549e16` | | Source Sans 3 400 | `7ebe648623e3aff6502dacdcaf774a27` | | Source Sans 3 700 | `7ebe648623e3aff6502dacdcaf774a27` | The three `@font-face` blocks for Open Sans (weights 300, 400, 700) all embed the exact same WOFF2 binary. The two blocks for Source Sans 3 (weights 400, 700) are also identical to each other. This means: - `font-weight: 300` (light text) and `font-weight: 700` (bold text) will both render using whichever single weight was accidentally embedded. Card visual quality is degraded — bold headings will not actually be bold. - The same bug exists in the source font files at `storage/fonts/`: `open-sans-300.woff2`, `open-sans-400.woff2`, and `open-sans-700.woff2` are all the same file (MD5 `dcf31ebe107435bd68e0164d59e19b87`). The font files themselves are incorrect, so this is not a base64 encoding error in the template — the wrong source files were used. **The fix requires:** 1. Downloading the correct WOFF2 binaries for each weight variant. Open Sans weight files differ substantially in size and glyph set. The correct files should be sourced from Google Fonts or the official Open Sans/Source Sans 3 releases. 2. Re-generating the base64 embeddings from the corrected WOFF2 files. 3. Replacing all five `src:` lines in `style.html` with the correct payloads. #### Security - No issues found. Removing the Google Fonts CDN links is the right move — it eliminates an external dependency and any associated privacy/availability concerns for Playwright rendering. #### Style & Conventions - The `@font-face` declarations follow valid CSS syntax and include the correct `format("woff2")` specifier. - Removing the three `<link>` tags from `player_card.html` is clean and correct. - The PR description's acceptance criteria checklist is technically satisfied (5 declarations, base64 URIs present, no CDN links) but does not verify that each weight's payload is distinct — that gap allowed this bug through. #### Suggestions - After sourcing the correct WOFF2 files, add a test that hashes (or at minimum checks the file size of) each embedded font blob to assert they are distinct. Something as simple as asserting that the five base64 strings are all different would have caught this. - Consider committing the WOFF2 source files to `storage/fonts/` in git (if file size allows) so the embedded data can be audited and regenerated deterministically. ### Verdict: REQUEST_CHANGES The goal of this PR (eliminating CDN dependencies) is correct and the approach is sound. However, all three Open Sans weight variants are encoded from an identical source file, as are both Source Sans 3 variants. Card rendering will silently use a single font weight for all bold/light/regular text. The correct WOFF2 files must be sourced and the base64 payloads regenerated before this can merge. --- *Automated review by Claude PR Reviewer*
@ -1,4 +1,29 @@
<style>
@font-face {
font-family: "Open Sans";
Owner

Open Sans 300, 400, and 700 all embed an identical base64 payload (MD5: c319fb108e28e5dec54276b666549e16). These three declarations must use distinct WOFF2 binaries corresponding to each weight.

Open Sans 300, 400, and 700 all embed an identical base64 payload (MD5: c319fb108e28e5dec54276b666549e16). These three declarations must use distinct WOFF2 binaries corresponding to each weight.
@ -2,0 +15,4 @@
src: url("data:font/woff2;base64,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
}
@font-face {
font-family: "Source Sans 3";
Owner

Source Sans 3 400 and 700 embed an identical base64 payload (MD5: 7ebe648623e3aff6502dacdcaf774a27). These two declarations must use distinct WOFF2 binaries.

Source Sans 3 400 and 700 embed an identical base64 payload (MD5: 7ebe648623e3aff6502dacdcaf774a27). These two declarations must use distinct WOFF2 binaries.
cal changed target branch from next-release to main 2026-03-23 19:51:23 +00:00
This pull request has changes conflicting with the target branch.
  • storage/templates/player_card.html
  • storage/templates/style.html

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin ai/paper-dynasty-database#88:ai/paper-dynasty-database#88
git checkout ai/paper-dynasty-database#88
Sign in to join this conversation.
No description provided.