Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 128 additions & 120 deletions apps/website/public/whitepaper-preview.html

Large diffs are not rendered by default.

Binary file modified apps/website/public/whitepaper.pdf
Binary file not shown.
284 changes: 146 additions & 138 deletions apps/website/public/whitepapers/angular-preview.html

Large diffs are not rendered by default.

Binary file modified apps/website/public/whitepapers/angular.pdf
Binary file not shown.
168 changes: 88 additions & 80 deletions apps/website/public/whitepapers/chat-preview.html

Large diffs are not rendered by default.

Binary file modified apps/website/public/whitepapers/chat.pdf
Binary file not shown.
204 changes: 106 additions & 98 deletions apps/website/public/whitepapers/render-preview.html

Large diffs are not rendered by default.

Binary file modified apps/website/public/whitepapers/render.pdf
Binary file not shown.
61 changes: 38 additions & 23 deletions apps/website/scripts/build-card-fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,29 @@
Satori (the engine behind Next.js ImageResponse) cannot decode woff2, which is
the only format Google Fonts serves, and it crashes on variable-weight TTFs
with "Cannot read properties of undefined (reading '256')". So every face a
card uses has to be instanced to a single weight, stripped of its variable
tables, and committed.

Until now only Garamond was bundled (see instance-garamond.py, which this
script supersedes). Inter and JetBrains Mono were scraped from the Google
Fonts CSS API on every card render. That is a network round trip inside an
image render, and when it fails there is no error — the card silently falls
back to whatever loaded, which is how a card whose eyebrow and pills are
specified in mono came out set in serif. Bundling removes the dependency.
card uses has to reach the renderer as a single static weight, stripped of its
variable tables, and committed.

Every face a card uses is bundled here rather than scraped from the Google
Fonts CSS API at render time. That was a network round trip inside an image
render, and when it failed there was no error — the card silently fell back to
whatever loaded, which is how a card whose eyebrow and pills are specified in
mono came out set in serif. Bundling removes the dependency.

The faces are the site's own: Archivo Black for display type, Archivo for
body, JetBrains Mono for the eyebrow and pills. Archivo Black is shipped by
Google as a *static* font — it has no `fvar` — so instancing is conditional;
running the instancer over it would fail rather than no-op. Archivo's variable
source carries a `wdth` axis alongside `wght`, which has to be pinned too, or
variable tables survive into the output and Satori chokes on them.

The fonts are subsetted to Latin plus the punctuation the site actually uses,
which is what keeps four faces under 150KB total rather than well over 1MB.
Blog post titles are the only unbounded text on a card; anything outside this
range falls back to Satori's bundled Noto Sans rather than failing.

Usage:
pip install --user fonttools brotli
pip install --user fonttools # no brotli: this reads and writes TTF
python3 apps/website/scripts/build-card-fonts.py

Re-run if an upstream font is updated, and commit the result.
Expand All @@ -45,18 +51,19 @@

FACES = [
{
"name": "EBGaramond-Bold.ttf",
"url": "https://github.com/google/fonts/raw/main/ofl/ebgaramond/EBGaramond%5Bwght%5D.ttf",
"weight": 700,
# Static — no `fvar`, so `build()` skips instancing for this one.
"name": "ArchivoBlack-Regular.ttf",
"url": "https://github.com/google/fonts/raw/main/ofl/archivoblack/ArchivoBlack-Regular.ttf",
"weight": 400,
},
{
"name": "Inter-Regular.ttf",
"url": "https://github.com/google/fonts/raw/main/ofl/inter/Inter%5Bopsz,wght%5D.ttf",
"name": "Archivo-Regular.ttf",
"url": "https://github.com/google/fonts/raw/main/ofl/archivo/Archivo%5Bwdth,wght%5D.ttf",
"weight": 400,
},
{
"name": "Inter-SemiBold.ttf",
"url": "https://github.com/google/fonts/raw/main/ofl/inter/Inter%5Bopsz,wght%5D.ttf",
"name": "Archivo-SemiBold.ttf",
"url": "https://github.com/google/fonts/raw/main/ofl/archivo/Archivo%5Bwdth,wght%5D.ttf",
"weight": 600,
},
{
Expand All @@ -75,12 +82,20 @@ def build(face: dict) -> None:
raw = tmp.name

font = TTFont(raw)
axes = {"wght": face["weight"]}
# Inter carries an optical-size axis as well; pin it to its text setting so
# instancing leaves no variable tables behind for Satori to trip over.
if "fvar" in font and any(a.axisTag == "opsz" for a in font["fvar"].axes):
axes["opsz"] = 14
font = instantiateVariableFont(font, axes, updateFontNames=False, inplace=True)
# Archivo Black ships static, with no `fvar`. Running the instancer over a
# font with no axes is not a harmless no-op, so only instance when there is
# something to instance. Every *other* axis the source carries has to be
# pinned as well, or its variation tables survive for Satori to trip over.
# Archivo carries `wdth` (pinned to the normal width); `opsz` is handled
# too, since Google ships several text faces with an optical-size axis.
if "fvar" in font:
tags = {a.axisTag for a in font["fvar"].axes}
axes = {"wght": face["weight"]}
if "wdth" in tags:
axes["wdth"] = 100
if "opsz" in tags:
axes["opsz"] = 14
font = instantiateVariableFont(font, axes, updateFontNames=False, inplace=True)
for table in ("fvar", "STAT", "MVAR", "HVAR", "VVAR", "gvar", "cvar", "avar"):
if table in font:
del font[table]
Expand Down
Loading
Loading