mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 09:48:47 -04:00
- Introduced `fetch-fonts.mjs` to download brand web fonts from Google Fonts into `public/fonts/`. - Integrated font fetching into the `still:og` script to ensure rendering dependencies are prepared automatically. - Updated `package.json`, README, and font-related modules to document usage and improve reproducibility. - Ensured rendering fails cleanly if fonts are missing or invalid, maintaining visual consistency.
135 lines
4.6 KiB
JavaScript
135 lines
4.6 KiB
JavaScript
/**
|
|
* Downloads the brand web faces into public/fonts/ for the OG card render.
|
|
*
|
|
* The files are not committed (see .gitignore) — the same call the repo makes for the hero clips
|
|
* and for public/logo.svg. They are build *inputs* though, not outputs, so they come from Google
|
|
* Fonts rather than from R2: no credentials, and nothing to upload when a face changes.
|
|
*
|
|
* Resolved through the css2 API rather than hardcoded gstatic URLs on purpose. Those URLs carry a
|
|
* family version (`/newsreader/v26/`) that Google rotates, so a hardcoded list rots silently; the
|
|
* API always answers with the current one.
|
|
*
|
|
* npm run fonts # no-op if the files are already there
|
|
* npm run fonts -- --force # re-download, e.g. to pick up a new family version
|
|
*
|
|
* `npm run still:og` runs this first, so rendering the card needs no separate step. Run it by hand
|
|
* before `npm run studio` if you want to preview the card there.
|
|
*
|
|
* Only Newsreader, DM Sans and Cormorant Garamond are fetched, and only their `latin` subset —
|
|
* that is all the card sets. The video scenes deliberately use the system stack instead.
|
|
*/
|
|
import {mkdir, readFile, writeFile} from 'node:fs/promises'
|
|
import {dirname, join} from 'node:path'
|
|
import {fileURLToPath} from 'node:url'
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
const FONT_DIR = join(HERE, '../public/fonts')
|
|
|
|
// A desktop UA is what makes the API answer with woff2; older agents get ttf.
|
|
const UA =
|
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
|
|
|
|
/**
|
|
* Filenames are what src/components/BrandFonts.ts loads — keep the two in sync.
|
|
*
|
|
* Every family here is variable, so one file per style covers the whole weight range and the
|
|
* weights in the query only decide which axes Google keeps.
|
|
*/
|
|
const FONTS = [
|
|
{
|
|
file: 'Newsreader-latin.woff2',
|
|
family: 'Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,600;0,6..72,700',
|
|
style: 'normal',
|
|
},
|
|
{
|
|
file: 'Newsreader-Italic-latin.woff2',
|
|
family: 'Newsreader:ital,opsz,wght@1,6..72,400',
|
|
style: 'italic',
|
|
},
|
|
{
|
|
file: 'DMSans-latin.woff2',
|
|
family: 'DM+Sans:opsz,wght@9..40,400;9..40,500',
|
|
style: 'normal',
|
|
},
|
|
{
|
|
file: 'CormorantGaramond-500-latin.woff2',
|
|
family: 'Cormorant+Garamond:wght@500',
|
|
style: 'normal',
|
|
},
|
|
]
|
|
|
|
const force = process.argv.includes('--force')
|
|
|
|
const exists = async (path) => {
|
|
try {
|
|
return (await readFile(path)).byteLength > 0
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pulls the `latin` @font-face of the requested style out of a css2 response.
|
|
*
|
|
* The API returns one block per subset, each preceded by a `/* subset *\/` comment — that comment
|
|
* is the only thing identifying them, so the parse keys off it.
|
|
*/
|
|
function findLatinSrc(css, style) {
|
|
const blocks = [...css.matchAll(/\/\*\s*([\w-]+)\s*\*\/\s*@font-face\s*\{([^}]*)\}/g)]
|
|
|
|
for (const [, subset, body] of blocks) {
|
|
if (subset !== 'latin') continue
|
|
const blockStyle = body.match(/font-style:\s*([\w]+)/)?.[1] ?? 'normal'
|
|
if (blockStyle !== style) continue
|
|
const url = body.match(/url\((https:\/\/[^)]+\.woff2)\)/)?.[1]
|
|
if (url) return url
|
|
}
|
|
return null
|
|
}
|
|
|
|
async function fetchFont({file, family, style}) {
|
|
const target = join(FONT_DIR, file)
|
|
|
|
if (!force && (await exists(target))) {
|
|
console.log(`[fonts] ${file} — already present`)
|
|
return
|
|
}
|
|
|
|
const cssUrl = `https://fonts.googleapis.com/css2?family=${family}&display=swap`
|
|
const cssRes = await fetch(cssUrl, {headers: {'User-Agent': UA}})
|
|
if (!cssRes.ok) {
|
|
throw new Error(`${cssRes.status} ${cssRes.statusText} for ${cssUrl}`)
|
|
}
|
|
|
|
const src = findLatinSrc(await cssRes.text(), style)
|
|
if (!src) {
|
|
throw new Error(`No latin ${style} woff2 in the css2 response for ${family}`)
|
|
}
|
|
|
|
const fontRes = await fetch(src, {headers: {'User-Agent': UA}})
|
|
if (!fontRes.ok) {
|
|
throw new Error(`${fontRes.status} ${fontRes.statusText} for ${src}`)
|
|
}
|
|
|
|
const bytes = Buffer.from(await fontRes.arrayBuffer())
|
|
// Guard against a redirect or an error page landing on disk as a .woff2: the render would fail
|
|
// much further away from the cause.
|
|
if (bytes.subarray(0, 4).toString('ascii') !== 'wOF2') {
|
|
throw new Error(`${src} did not return a woff2 file`)
|
|
}
|
|
|
|
await writeFile(target, bytes)
|
|
console.log(`[fonts] ${file} (${(bytes.byteLength / 1024).toFixed(0)} KB)`)
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(FONT_DIR, {recursive: true})
|
|
for (const font of FONTS) await fetchFont(font)
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`[fonts] ${err.message}`)
|
|
console.error('[fonts] The OG card cannot render without these — see media-creator/README.md.')
|
|
process.exit(1)
|
|
})
|