// Renders the frontpage's social card (the image behind Open Graph / Twitter // link unfurls) from the brand SVGs, on the same pipeline as the store // tiles: composed at render time from brand/sticker.svg and the generated // wordmark, drawn by a headless Chrome, and verified before writing. // // 1200x630 is the one size the major unfurl surfaces (Bluesky, Slack, // Discord, iMessage, X) all display without cropping the safe area. // // Run npm run wordmark first when brand sources change (the prek hooks // order this correctly): the card nests the generated wordmark file. // // Usage: node scripts/render-og.mjs (also: npm run og) // Set CHROME to point at a specific Chrome/Chromium binary. import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' import { compose, nest, nestWordmark } from './compose.mjs' import { findChrome, pngSize } from './render-icons.mjs' import { rasterize } from './rasterize.mjs' const CARD = { width: 1200, height: 630, dest: 'web/public/og.png', body: () => nest('brand/sticker.svg', 120, 145, 340) + nestWordmark(540, 315, 560), } function main() { const chrome = findChrome() const work = mkdtempSync(join(tmpdir(), 'substandard-og-')) try { const { opaque, png } = rasterize( chrome, work, 'og', compose(CARD), CARD.width, CARD.height, ) const dim = pngSize(png) if (dim.width !== CARD.width || dim.height !== CARD.height) { throw new Error(`og: rendered ${dim.width}x${dim.height}`) } // The paper ground makes a healthy card fully opaque. const fraction = opaque / (CARD.width * CARD.height) if (fraction < 0.99) { throw new Error(`og: only ${(fraction * 100).toFixed(1)}% opaque — misrender`) } writeFileSync(resolve(CARD.dest), png) console.log(`og: ${png.length} bytes -> ${CARD.dest}`) } finally { rmSync(work, { recursive: true, force: true }) } } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main() }