mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 09:48:47 -04:00
Add script for fetching web fonts for OG card rendering
- 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.
This commit is contained in:
4
media-creator/.gitignore
vendored
4
media-creator/.gitignore
vendored
@@ -17,3 +17,7 @@ npm-debug.log*
|
||||
# Captured marketing stills (regenerate with the capture scripts).
|
||||
public/search/
|
||||
public/alert/
|
||||
|
||||
# Brand web faces for the OG card. Build inputs, not ours to redistribute — refetch from Google
|
||||
# with `npm run fonts` (which `npm run still:og` does for you).
|
||||
public/fonts/
|
||||
|
||||
@@ -18,14 +18,25 @@ own dependency tree so the web app and API never pull Remotion (and its bundled
|
||||
```bash
|
||||
cd media-creator
|
||||
npm install # first time only — also downloads a headless browser for rendering
|
||||
npm run fonts # brand web faces for the OG card; uncommitted, see Stills below
|
||||
```
|
||||
|
||||
Two render inputs are not in the repo and are not installed by npm:
|
||||
|
||||
- `public/fonts/` — fetched by `npm run fonts` (and automatically by `npm run still:og`).
|
||||
- `public/logo.svg` — a copy of `web/public/favicon.svg`, which the root `.gitignore` excludes along
|
||||
with every other `*.svg`. Copy it by hand: `cp ../web/public/favicon.svg public/logo.svg`. Every
|
||||
scene that draws the logo needs it.
|
||||
|
||||
## Preview (interactive studio)
|
||||
|
||||
```bash
|
||||
npm run studio # opens the Remotion Studio at http://localhost:3000
|
||||
```
|
||||
|
||||
> Previewing `OgCard` in the studio needs `npm run fonts` first — the studio does not run it, and
|
||||
> the render fails loudly rather than falling back to a substitute face.
|
||||
|
||||
## Render to MP4
|
||||
|
||||
The same intro renders into two Instagram-ready formats:
|
||||
@@ -137,11 +148,24 @@ cannot promise something the landing page does not say. If the home headline cha
|
||||
#### Fonts
|
||||
|
||||
This is the one scene that uses the real web faces — Newsreader for the headline, DM Sans for the
|
||||
eyebrow, Cormorant Garamond for the wordmark — rather than the system stack the videos use. The
|
||||
woff2 files are vendored in `public/fonts/` (both families are SIL OFL) and loaded by
|
||||
`src/components/BrandFonts.ts`, which holds the render open until they are ready. Vendoring rather
|
||||
than fetching from Google keeps the render reproducible offline; a missing network would otherwise
|
||||
silently produce a card in Georgia.
|
||||
eyebrow, Cormorant Garamond for the wordmark — rather than the system stack the videos use.
|
||||
|
||||
The woff2 files live in `public/fonts/` and are **not committed** (`.gitignore`), like the hero clips
|
||||
and `public/logo.svg`. Fetch them with:
|
||||
|
||||
```bash
|
||||
npm run fonts # no-op when they are already there
|
||||
npm run fonts -- --force # re-download, e.g. to pick up a new family version
|
||||
```
|
||||
|
||||
`npm run still:og` runs this itself, so rendering the card is a single command. `scripts/fetch-fonts.mjs`
|
||||
resolves the download URLs through the Google Fonts css2 API rather than hardcoding gstatic paths:
|
||||
those carry a family version (`/newsreader/v26/`) that Google rotates, so a hardcoded list would rot
|
||||
silently.
|
||||
|
||||
`src/components/BrandFonts.ts` registers the faces and holds the render open until the browser
|
||||
reports them ready. A missing or corrupt file **cancels the render** instead of falling back to
|
||||
Georgia — a card in the wrong typeface would ship unnoticed, a red render will not.
|
||||
|
||||
## Structure
|
||||
|
||||
@@ -149,8 +173,8 @@ silently produce a card in Georgia.
|
||||
media-creator/
|
||||
├── package.json standalone deps (npm)
|
||||
├── remotion.config.ts render settings (codec, quality)
|
||||
├── public/logo.svg Compass logo (copied from web/public/favicon.svg)
|
||||
├── public/fonts/ vendored web faces, used by the OG card only
|
||||
├── public/logo.svg Compass logo (copied from web/public/favicon.svg; uncommitted)
|
||||
├── public/fonts/ brand web faces, OG card only (npm run fonts; uncommitted)
|
||||
└── src/
|
||||
├── index.ts registerRoot
|
||||
├── Root.tsx composition registry
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"capture:search": "node scripts/capture-search.mjs --theme light",
|
||||
"capture:search:dark": "node scripts/capture-search.mjs --theme dark",
|
||||
"capture:vote": "node scripts/capture-vote.mjs",
|
||||
"fonts": "node scripts/fetch-fonts.mjs",
|
||||
"render": "npm run render:post",
|
||||
"render:alert": "remotion render SearchAlertLight out/compass-search-alert-light.mp4 --crf 30",
|
||||
"render:alert:dark": "remotion render SearchAlertDark out/compass-search-alert-dark.mp4 --crf 30",
|
||||
@@ -23,7 +24,7 @@
|
||||
"render:tour:post": "remotion render ProfileTourPost out/compass-profile-tour-post.mp4",
|
||||
"still:alert": "remotion still SearchAlertLight out/compass-alert-poster-light.png --frame=0",
|
||||
"still:alert:dark": "remotion still SearchAlertDark out/compass-alert-poster-dark.png --frame=0",
|
||||
"still:og": "remotion still OgCard out/compass-og-card.jpg --image-format=jpeg --jpeg-quality=92",
|
||||
"still:og": "npm run fonts && remotion still OgCard out/compass-og-card.jpg --image-format=jpeg --jpeg-quality=92",
|
||||
"still:search": "remotion still SearchDemoLight out/compass-search-poster-light.png --frame=0",
|
||||
"still:search:dark": "remotion still SearchDemoDark out/compass-search-poster-dark.png --frame=0",
|
||||
"studio": "remotion studio",
|
||||
|
||||
134
media-creator/scripts/fetch-fonts.mjs
Normal file
134
media-creator/scripts/fetch-fonts.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
@@ -1,9 +1,10 @@
|
||||
// Loads the real Compass web faces so a render looks like the site rather than like the local
|
||||
// system-font stack.
|
||||
//
|
||||
// The woff2 files are vendored under public/fonts (both families are SIL OFL) rather than fetched
|
||||
// from Google at render time — theme.ts's rule is that a render must be reproducible offline, and a
|
||||
// missing network would otherwise silently fall back to Georgia mid-render.
|
||||
// The woff2 files are read from public/fonts, which is uncommitted: `npm run fonts` downloads them
|
||||
// from Google Fonts, and `npm run still:og` runs that first. Fetching once at setup rather than at
|
||||
// render time is what keeps theme.ts's rule intact — a render itself touches no network, so it
|
||||
// cannot silently fall back to Georgia halfway through.
|
||||
//
|
||||
// Importing this module registers the faces and holds the render open until the browser reports
|
||||
// them ready. Only scenes that need them should import it; the video scenes deliberately do not,
|
||||
|
||||
@@ -33,8 +33,8 @@ export const fonts = {
|
||||
serif: 'Georgia, "Times New Roman", serif',
|
||||
|
||||
// The real web faces (see web/pages/_document.tsx and tailwind.config.js). Only usable in a
|
||||
// scene that imports ./components/BrandFonts — that module vendors the woff2 files from
|
||||
// public/fonts and blocks the render until they are ready. Without it these fall back.
|
||||
// scene that imports ./components/BrandFonts — that module loads the woff2 files from
|
||||
// public/fonts (`npm run fonts`) and blocks the render until they are ready.
|
||||
heading: '"Newsreader", Georgia, serif', // h1–h6 on the site
|
||||
body: '"DM Sans", "Segoe UI", sans-serif', // body copy
|
||||
wordmark: '"Cormorant Garamond", Georgia, serif', // the `.logo` wordmark
|
||||
|
||||
Reference in New Issue
Block a user