Skip to content

Commit b08a817

Browse files
bloveclaude
andcommitted
feat(website): EB Garamond headline on OG card (item 2 follow-up)
Bundles a static-weight EB Garamond Bold TTF next to opengraph-image.tsx so the share card headline renders in the marketing-site h1 typeface instead of falling back to Inter Bold. Why the TTF lives in the repo: - Google Fonts only serves Garamond as woff2, which Satori (next/og's underlying renderer) cannot decode. - The upstream variable-weight TTF trips Satori's parser with "Cannot read properties of undefined (reading '256')" on the variable font tables (fvar/STAT/MVAR/HVAR). Solution: instance the upstream variable font to wght=700 using fontTools' instancer, then drop the unused variable-font tables. Output is a clean static-weight TTF (~500KB) that Satori parses without issue. New apps/website/scripts/instance-garamond.py reproduces the TTF from upstream — re-run after any upstream font update. Also flips the route's runtime from 'edge' to 'nodejs' so the font file can be read off disk via fileURLToPath(import.meta.url). All 35 website e2e tests pass. Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent e76b1f0 commit b08a817

3 files changed

Lines changed: 95 additions & 2 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Generate apps/website/src/app/EBGaramond-Bold.ttf from the upstream EB
3+
Garamond variable font.
4+
5+
Why this script exists:
6+
- Satori (the engine behind Next.js ImageResponse) crashes on variable-weight
7+
TTFs with "Cannot read properties of undefined (reading '256')".
8+
- Google Fonts only serves Garamond as woff2, which Satori also can't decode.
9+
- So we instance the upstream variable font to a single weight (Bold, 700)
10+
and strip the now-unused variable-font tables, producing a static TTF
11+
Satori parses cleanly.
12+
13+
The output is committed to the repo and consumed by
14+
apps/website/src/app/opengraph-image.tsx at request time.
15+
16+
Usage:
17+
pip install --user fonttools
18+
python3 apps/website/scripts/instance-garamond.py
19+
20+
Re-run if the upstream font is updated.
21+
"""
22+
import os
23+
import tempfile
24+
import urllib.request
25+
26+
from fontTools.ttLib import TTFont
27+
from fontTools.varLib.instancer import instantiateVariableFont
28+
29+
UPSTREAM_URL = "https://github.com/google/fonts/raw/main/ofl/ebgaramond/EBGaramond%5Bwght%5D.ttf"
30+
TARGET_WEIGHT = 700
31+
OUTPUT_PATH = os.path.join(
32+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
33+
"src",
34+
"app",
35+
"EBGaramond-Bold.ttf",
36+
)
37+
38+
39+
def main() -> None:
40+
print(f"Downloading variable TTF from {UPSTREAM_URL}")
41+
with tempfile.NamedTemporaryFile(suffix=".ttf", delete=False) as tmp:
42+
with urllib.request.urlopen(UPSTREAM_URL) as response:
43+
tmp.write(response.read())
44+
src_path = tmp.name
45+
46+
try:
47+
print(f"Instancing to wght={TARGET_WEIGHT}")
48+
font = TTFont(src_path)
49+
static = instantiateVariableFont(font, {"wght": TARGET_WEIGHT})
50+
51+
# Drop variable-font tables that no longer serve a purpose and that
52+
# Satori doesn't need. Shaves ~300KB off the file.
53+
for tag in ("STAT", "fvar", "MVAR", "HVAR"):
54+
if tag in static:
55+
del static[tag]
56+
57+
print(f"Writing {OUTPUT_PATH}")
58+
static.save(OUTPUT_PATH)
59+
size_kb = os.path.getsize(OUTPUT_PATH) // 1024
60+
print(f"Done — {size_kb} KB")
61+
finally:
62+
os.unlink(src_path)
63+
64+
65+
if __name__ == "__main__":
66+
main()
509 KB
Binary file not shown.

apps/website/src/app/opengraph-image.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
*/
88
import { ImageResponse } from 'next/og';
99

10-
export const runtime = 'edge';
10+
// Node runtime (not edge) so we can read the bundled Garamond TTF off disk.
11+
export const runtime = 'nodejs';
1112
export const alt = 'Angular Agent Framework — Signal-native streaming for Angular + LangGraph';
1213
export const size = { width: 1200, height: 630 };
1314
export const contentType = 'image/png';
@@ -31,9 +32,35 @@ async function loadFont(family: string, weight: number): Promise<ArrayBuffer | n
3132
}
3233
}
3334

35+
/**
36+
* EB Garamond is bundled as a static-weight TTF next to this file because:
37+
* 1. Google Fonts only serves Garamond as woff2 — Satori can't decode woff2.
38+
* 2. The variable-weight TTF in Google's fonts repo trips Satori's TTF parser
39+
* ("Cannot read properties of undefined (reading '256')") on variable-font
40+
* tables (fvar/STAT/MVAR/HVAR).
41+
*
42+
* The committed TTF was produced by instancing the upstream variable font to
43+
* wght=700 and stripping the now-unused variable tables — see
44+
* apps/website/scripts/instance-garamond.py. The file is ~500KB, served only
45+
* from this server-side render path (never downloaded by browsers).
46+
*/
47+
async function loadLocalGaramond(): Promise<ArrayBuffer | null> {
48+
try {
49+
const { fileURLToPath } = await import('node:url');
50+
const { readFile } = await import('node:fs/promises');
51+
const { dirname, join } = await import('node:path');
52+
const here = dirname(fileURLToPath(import.meta.url));
53+
const buf = await readFile(join(here, 'EBGaramond-Bold.ttf'));
54+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
55+
} catch (err) {
56+
console.warn('opengraph-image: failed to load local Garamond TTF', err);
57+
return null;
58+
}
59+
}
60+
3461
export default async function OpenGraphImage() {
3562
const [garamondBold, interRegular, interBold, monoBold] = await Promise.all([
36-
loadFont('EB+Garamond', 700),
63+
loadLocalGaramond(),
3764
loadFont('Inter', 400),
3865
loadFont('Inter', 600),
3966
loadFont('JetBrains+Mono', 700),

0 commit comments

Comments
 (0)