#!/usr/bin/env python3 """Download the web fonts from Google Fonts and vendor them into the repo. Run by hand when the font set changes, never at build or run time: python3 tools/vendor-fonts.py The app must render itself with no outbound network, so nothing here may happen while it is running. This script fetches once, writes the woff2 files and a stylesheet that points at them by RELATIVE url, and everything is committed. `web/static/` is copied verbatim into the SvelteKit build, which Go then embeds — so the fonts travel inside the binary. Relative urls in the stylesheet (`./Inter-latin.woff2`, not `/fonts/...`) are deliberate: a CSS url() resolves against the stylesheet's own address, so the whole directory keeps working when the app is served under a base path. Subsets are kept as Google slices them, with their `unicode-range` intact. That is not a size compromise — the browser downloads only the ranges a page actually uses, so vendoring every subset costs repository bytes rather than request bytes, and a library full of non-English artist names renders instead of falling back mid-list. """ import re import sys import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent.parent OUT = ROOT / "web/static/fonts" # Matches the faces the design system actually permits: two weights only, 400 # and 500 (never 600/700), and Fraunces' optical-size axis across its range. FAMILIES = ( "family=Fraunces:opsz,wght@9..144,400;9..144,500" "&family=Inter:wght@400;500" "&family=JetBrains+Mono:wght@400;500" ) CSS_URL = f"https://fonts.googleapis.com/css2?{FAMILIES}&display=swap" # A modern browser UA is required, not cosmetic: Google serves ancient TTF to # unrecognised clients and woff2 only to browsers known to support it. UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36") def fetch(url, timeout=30): req = urllib.request.Request(url, headers={"User-Agent": UA}) with urllib.request.urlopen(req, timeout=timeout) as r: return r.read() def main(): css = fetch(CSS_URL).decode() # Google emits a comment naming each subset before its @font-face block. blocks = re.split(r"(?=/\*\s*[a-z0-9-]+\s*\*/)", css) OUT.mkdir(parents=True, exist_ok=True) for old in OUT.glob("*.woff2"): old.unlink() out_css, seen, total = [], {}, 0 for block in blocks: subset = re.search(r"/\*\s*([a-z0-9-]+)\s*\*/", block) family = re.search(r"font-family:\s*'([^']+)'", block) weight = re.search(r"font-weight:\s*([^;]+);", block) url = re.search(r"url\((https://[^)]+\.woff2)\)", block) if not (subset and family and url): continue w = weight.group(1).strip().replace(" ", "-") if weight else "400" name = f"{family.group(1).replace(' ', '')}-{w}-{subset.group(1)}.woff2" if name not in seen: data = fetch(url.group(1)) (OUT / name).write_bytes(data) seen[name] = len(data) total += len(data) out_css.append(block.replace(url.group(1), f"./{name}")) header = ( "/* Generated by tools/vendor-fonts.py — do not edit by hand.\n" " *\n" " * Vendored from Google Fonts and served from our own origin: the app\n" " * has to render with no outbound network, so a font provider link is\n" " * not an option. Urls are relative so this keeps working under a base\n" " * path. unicode-range is preserved, so the browser still fetches only\n" " * the subsets a page needs.\n" " */\n" ) (OUT / "fonts.css").write_text(header + "".join(out_css)) for n, size in sorted(seen.items()): print(f" {n:<44} {size/1024:7.1f} KB") print(f"{len(seen)} files, {total/1024/1024:.2f} MB total -> {OUT.relative_to(ROOT)}") if __name__ == "__main__": sys.exit(main())