#!/usr/bin/env node /** * Generate OG Image (Neo Kinpaku brand) * * Renders the social sharing card with Playwright using the real Kinpaku * tokens (lacquer ground, champagne headline, kinpaku-gold accent) and the * kintsugi-seam hero art. Renders at 2x and downscales with sharp for crisp * text. The command count is read live from command-metadata.json so it can * never go stale. * * Output: site/public/og-image-v2.jpg (the cache-busted filename Base.astro * and index.astro reference). Bump the version suffix here and in those two * files together when you want social scrapers to re-fetch a fresh card. * * Usage: bun run og-image */ import { chromium } from 'playwright'; import sharp from 'sharp'; import os from 'os'; import path from 'path'; import fs from 'fs'; import { fileURLToPath, pathToFileURL } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ROOT_DIR = path.resolve(__dirname, '..'); const OUTPUT_PATH = path.join(ROOT_DIR, 'site', 'public', 'og-image-v2.jpg'); const ART_PATH = path.join( ROOT_DIR, 'site', 'public', 'assets', 'neo-kinpaku', 'candidates', 'finalists', 'm-01-v2-01.png', ); // Count sub-commands from skill/scripts/command-metadata.json (the post-v3.0 // single source of truth), so the card's "N commands" tracks the real total. function getCommandCount() { const metadataPath = path.join(ROOT_DIR, 'skill', 'scripts', 'command-metadata.json'); if (!fs.existsSync(metadataPath)) return 0; const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8')); return Object.keys(metadata).length; } async function generateOgImage() { const commands = getCommandCount(); // Reference the art by file:// URL (not base64): goto + networkidle then // genuinely waits for it to load, where a data URL emits no network event // and paints black before it decodes. const artUrl = pathToFileURL(ART_PATH).href; console.log(`Detected ${commands} command(s)`); const html = `
Impeccable

Design fluency for
every AI harness.

Stop shipping generic frontend. A design skill, CLI, and Chrome extension for the tools you already build with.

${commands} commands·Skill·CLI·Extension
impeccable.style
`; const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 2, }); // Write to a temp file and load via file:// so networkidle waits for the // art (a file:// page can reference file:// resources; data: cannot). const tmpHtml = path.join(os.tmpdir(), `impeccable-og-${process.pid}.html`); fs.writeFileSync(tmpHtml, html); try { await page.goto(pathToFileURL(tmpHtml).href, { waitUntil: 'networkidle' }); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(200); // Screenshot at 2x (2400x1260), then downscale to 1200x630 for crisp text. const buf = await page.screenshot({ clip: { x: 0, y: 0, width: 1200, height: 630 } }); await browser.close(); await sharp(buf).resize(1200, 630).jpeg({ quality: 86 }).toFile(OUTPUT_PATH); } finally { fs.rmSync(tmpHtml, { force: true }); } const size = (fs.statSync(OUTPUT_PATH).size / 1024).toFixed(0); console.log(`Generated ${OUTPUT_PATH} (${size} KB)`); } generateOgImage().catch((err) => { console.error('Failed to generate OG image:', err); process.exit(1); });