// Renders the 1200x630 social preview (/og.jpg) with headless Chrome, the same way the // Asteroids and Roadster share images are made: the art is HTML/SVG plus a real screenshot // of the running app (real keys, real range strip, real middle C), and every word is live // text, so nothing is AI generated. Writes src/og-image.ts (base64) and /tmp/pocket-piano-og.jpg. // // Usage: node scripts/render-og.mjs [appUrl] // appUrl defaults to https://piano.kentcdodds.com/ (or a local `kody dev --app-base-path /` URL). // Needs playwright-core resolvable from here (node_modules is gitignored) and a Chromium; // set CHROME_PATH to use a specific browser. Fonts: Playfair Display and Inter installed. import { chromium } from 'playwright-core' import { writeFileSync } from 'node:fs' const APP_URL = process.argv[2] || 'https://piano.kentcdodds.com/' // Shown at the bottom of the image. Pass your own app URL (or set OG_SITE_LABEL) in a fork. const SITE_LABEL = process.env.OG_SITE_LABEL || new URL(APP_URL).host const browser = await chromium.launch(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}) // 1. Screenshot the real app on a phone-sized screen, tenor part, a C major chord held. const phone = await browser.newPage({ viewport: { width: 400, height: 820 }, deviceScaleFactor: 3, isMobile: true, hasTouch: true }) await phone.addInitScript(() => { try { localStorage.setItem('pocket-piano:part', '"tenor"'); localStorage.setItem('pocket-piano:update-dismissed-all', '1') } catch (e) {} }) await phone.goto(APP_URL, { waitUntil: 'networkidle' }) await phone.waitForFunction(() => window.pocketPiano && window.pocketPiano.state.keyPx > 0) await phone.evaluate(() => { document.querySelectorAll('.update-toast').forEach((el) => el.remove()) const chord = [60, 64, 67] // C4 E4 G4, shown pressed on the top row const rows = document.querySelectorAll('#keyboard .row') const top = rows[0] chord.forEach((m) => top.querySelector('.key[data-midi="' + m + '"]').classList.add('down')) document.getElementById('readout-note').textContent = 'C4' document.getElementById('readout-hz').textContent = '261.6 Hz' document.getElementById('readout').classList.add('has-note') }) await phone.waitForTimeout(300) const shot = (await phone.screenshot({ type: 'png' })).toString('base64') await phone.close() // 2. Compose the scene. let seed = 9 const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647) const bokeh = Array.from({ length: 26 }, () => { const x = rnd() * 1200, y = 430 + rnd() * 220, r = 6 + rnd() * 26 const warm = rnd() > 0.35 return `
` }).join('') const dust = Array.from({ length: 90 }, () => ``).join('') // A glowing staff that sweeps behind the phone, with a few notes on it. const staff = [0, 1, 2, 3, 4].map((i) => `A piano in your pocket
for choir practice