'use strict' /** * render-nova-card.cjs * * Headless Playwright screenshot renderer for Nova Cards. * Visits the signed render-frame URL, waits for React + fonts to finish, * then screenshots the [data-card-canvas] element and writes a PNG to disk. * * Usage: * node scripts/render-nova-card.cjs \ * --url= \ * --out= \ * [--width=1080] [--height=1080] * * Exit codes: 0 = success, 1 = error (message on stderr). * On success writes JSON to stdout: { "success": true, "out": "...", "width": N, "height": N } */ const { chromium } = require('@playwright/test') const fs = require('fs') const path = require('path') // ── Parse CLI args ──────────────────────────────────────────────────────────── const argv = Object.fromEntries( process.argv.slice(2) .filter((a) => a.startsWith('--')) .map((a) => { const eq = a.indexOf('=') return eq === -1 ? [a.slice(2), true] : [a.slice(2, eq), a.slice(eq + 1)] }), ) const { url, out } = argv const width = parseInt(argv.width || '1080', 10) const height = parseInt(argv.height || '1080', 10) if (!url || !out) { process.stderr.write( 'Usage: render-nova-card.cjs --url= --out= [--width=1080] [--height=1080]\n', ) process.exit(1) } // ── Render ──────────────────────────────────────────────────────────────────── ;(async () => { const browser = await chromium.launch({ headless: true }) try { const page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 1, }) // Navigate and wait for network to settle (background images, fonts). await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 }) // Wait for React to finish painting the canvas element. const canvas = page.locator('[data-card-canvas]').first() await canvas.waitFor({ state: 'visible', timeout: 15_000 }) // Wait for all web fonts to finish loading before we capture layout. await page.evaluate(async () => { const requiredFonts = [ 'Anton', 'Caveat', 'Cormorant Garamond', 'Inter', 'Libre Franklin', 'Playfair Display', ] await Promise.all(requiredFonts.map((family) => document.fonts?.load?.(`16px "${family}"`) || Promise.resolve())) if (document.fonts?.ready) { await document.fonts.ready } }) // One extra frame so CSS transforms / container queries resolve fully. await page.waitForTimeout(300) // Screenshot only the canvas element; Playwright clips to the element bounding box. const png = await canvas.screenshot({ type: 'png' }) fs.mkdirSync(path.dirname(out), { recursive: true }) fs.writeFileSync(out, png) process.stdout.write(JSON.stringify({ success: true, out, width, height }) + '\n') } finally { await browser.close() } })().catch((err) => { process.stderr.write((err?.message ?? String(err)) + '\n') process.exit(1) })