Design fluency for
every AI harness.
+ Stop shipping generic frontend. A design skill, CLI, and Chrome extension for the tools you already build with.
+diff --git a/.gitignore b/.gitignore index 385f91464..502f49f54 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,12 @@ site/public/js/generated/ # Local-only scratch for exploratory scripts, parked pages, and unused asset candidates. tmp/ + +# Isolated ablation staging (impeccable-evals copies the built staged skill here +# per rule, removes one rule, points a worker at it). Throwaway; never committed. +.ablate-stage/ + +# Local scratch from iterating on the OG card (weight comparisons, intermediate +# PNGs, the old card backup). The canonical generator is `bun run og-image` +# (scripts/generate-og-image.js); this dir is throwaway and safe to delete. +.og-build/ diff --git a/CLAUDE.md b/CLAUDE.md index 96f8bdcd9..909b264f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,6 +88,20 @@ Hosted on Cloudflare Pages. Static assets served from `build/`, API routes handl bun run deploy # Build + deploy to Cloudflare Pages ``` +## Social sharing image (OG card) + +The OG / Twitter card is generated, not hand-drawn. To regenerate after a brand or copy change: + +```bash +bun run og-image # → site/public/og-image-v2.jpg +``` + +`scripts/generate-og-image.js` renders an inline HTML card with Playwright (Neo Kinpaku brand: lacquer ground, champagne Alumni Sans headline, kinpaku-gold accent, the kintsugi-seam art from `site/public/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png`). It renders at 2× and downscales to 1200×630 with `sharp` for crisp text. The "N commands" figure is read live from `command-metadata.json`, so it never goes stale; don't hardcode it. + +The card is referenced as a **sitewide default** in `site/layouts/Base.astro` (every page emits `og:image` + a `summary_large_image` Twitter card; pages may override via the `ogImage` prop). The homepage sets its own `ogImage` in `site/pages/index.astro`. + +**Cache-busting:** social scrapers cache by URL, so the filename carries a `-v2` suffix. When you ship a visibly different card, bump the suffix in three places together (`scripts/generate-og-image.js` `OUTPUT_PATH`, `Base.astro` `SITE_OG_IMAGE`, `index.astro` `ogImage`) so X/LinkedIn/Slack re-fetch instead of serving the stale image. After deploy, prime the caches by running the URL through X's Post Inspector and LinkedIn's Post Inspector once. + ## Build System The build system compiles the impeccable skill from `skill/` to provider-specific formats in `dist/`: diff --git a/scripts/generate-og-image.js b/scripts/generate-og-image.js index e22536425..8e37591bd 100644 --- a/scripts/generate-og-image.js +++ b/scripts/generate-og-image.js @@ -1,36 +1,39 @@ #!/usr/bin/env node /** - * Generate OG Image + * Generate OG Image (Neo Kinpaku brand) * - * Renders the OG image using Playwright with proper Google Fonts. - * Counts commands dynamically from the source/ directory and composes - * the wordmark alongside a real screenshot of the Chrome extension - * detection panel from public/assets/extension-detection.png. + * 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 } from 'url'; +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.jpg'); -const EXTENSION_IMAGE_PATH = path.join( +const OUTPUT_PATH = path.join(ROOT_DIR, 'site', 'public', 'og-image-v2.jpg'); +const ART_PATH = path.join( ROOT_DIR, - 'public', - 'assets', - 'extension-detection.png', + '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). Commands and skills were unified in v2.0; v3.0 -// then collapsed to a single user-invocable skill (`impeccable`) with -// sub-commands listed in command-metadata.json. +// 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; @@ -38,176 +41,101 @@ function getCommandCount() { return Object.keys(metadata).length; } -// Load extension screenshot as base64 data URL so setContent is self-contained -function getExtensionDataUrl() { - const buf = fs.readFileSync(EXTENSION_IMAGE_PATH); - return `data:image/png;base64,${buf.toString('base64')}`; -} - async function generateOgImage() { const commands = getCommandCount(); - const extensionDataUrl = getExtensionDataUrl(); + // 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 = ` - +
- - - - - + + + + + -Stop shipping generic frontend. A design skill, CLI, and Chrome extension for the tools you already build with.
+