// Self-host the deck's webfonts. // // The Slidev build links a cross-origin Google Fonts stylesheet, so document.styleSheets // cannot read it and the capture drops its @font-face rules with no error. On macOS // nothing looks wrong — Avenir Next is a system font — and everywhere else the deck // silently falls back to whatever the OS happens to have. // // Two facts, both measured: // · Google serves only Nunito Sans and Victor Mono for that URL. It IGNORES // `Avenir+Next` — a proprietary Linotype face, absent from Google Fonts and not // licensable for self-hosting. It stays in the stack as a local preference; the // stack's next entry is what we now guarantee for everyone else. (Decided 2026-07-14: // Mac readers see Avenir Next, everyone else Nunito Sans — a conscious split.) // · The User-Agent decides the format. A generic UA gets .ttf; a modern Chrome UA gets // .woff2, roughly a third of the bytes. import { writeFile, mkdir } from 'node:fs/promises' import { readFile } from 'node:fs/promises' import { basename, join } from 'node:path' import { DIST, postcss, publicBase, publicDir } from './paths.mjs' const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' // latin + latin-ext. Greek, cyrillic and vietnamese are subsets an es/en deck never // types. Select on the range's own start codepoint — the file hashes carry no meaning, // and guessing from them is how a whole family gets deleted by accident. const LATIN = /^U\+0000-00FF/i const LATIN_EXT = /^U\+0100-02BA/i const wantedRange = (r) => LATIN.test(r.trim()) || LATIN_EXT.test(r.trim()) export async function fetchFonts(id, { apply = true } = {}) { const index = await readFile(join(DIST, 'index.html'), 'utf8') const href = index.match(/]+href="(https:\/\/fonts\.googleapis\.com[^"]+)"/)?.[1] if (!href) return { skipped: 'el build no enlaza Google Fonts', css: '', files: [] } const res = await fetch(href.replaceAll('&', '&'), { headers: { 'User-Agent': UA } }) if (!res.ok) throw new Error(`Google Fonts: HTTP ${res.status}`) let css = await res.text() const requested = [...href.matchAll(/family=([^:&]+)/g)].map(m => decodeURIComponent(m[1]).replace(/\+/g, ' ')) const served = [...new Set([...css.matchAll(/font-family:\s*'([^']+)'/g)].map(m => m[1]))] const notServed = requested.filter(f => !served.includes(f)) const base = `${publicBase(id)}/fonts` const dir = join(publicDir(id), 'fonts') if (apply) await mkdir(dir, { recursive: true }) const root = postcss.parse(css) const keep = [] root.walkAtRules('font-face', at => { const src = at.toString() const range = src.match(/unicode-range:\s*([^;]+)/i)?.[1] if (range && !wantedRange(range)) { at.remove(); return } const url = src.match(/url\((https:\/\/fonts\.gstatic\.com[^)]+)\)/)?.[1] if (url) keep.push(url) }) const files = [] for (const url of [...new Set(keep)]) { const name = basename(new URL(url).pathname) if (apply) { const r = await fetch(url, { headers: { 'User-Agent': UA } }) if (!r.ok) throw new Error(`fuente ${url}: HTTP ${r.status}`) await writeFile(join(dir, name), Buffer.from(await r.arrayBuffer())) } files.push(name) } let out = root.toString() for (const url of [...new Set(keep)]) out = out.replaceAll(url, `${base}/${basename(new URL(url).pathname)}`) return { css: out, files, served, notServed } }