Refresh OG image with Chrome extension product shot

Replaces the brand-only card with a split layout: wordmark left,
floating Chrome extension detection panel right. Generator now counts
user-invocable, non-deprecated skills from source/skills/ (v2.0 unified
structure) instead of the removed source/commands/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-08 13:17:52 -07:00
co-authored by Claude Opus 4.6
parent 27d1b13bc2
commit 2a38fad925
2 changed files with 127 additions and 84 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 69 KiB

+127 -84
View File
@@ -4,7 +4,9 @@
* Generate OG Image
*
* Renders the OG image using Playwright with proper Google Fonts.
* Counts skills and commands dynamically from the source/ directory.
* 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.
*
* Usage: bun run og-image
*/
@@ -18,26 +20,48 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '..');
const OUTPUT_PATH = path.join(ROOT_DIR, 'public', 'og-image.jpg');
const EXTENSION_IMAGE_PATH = path.join(
ROOT_DIR,
'public',
'assets',
'extension-detection.png',
);
// Count skills and commands from source directory
function getCounts() {
// Count user-invocable, non-deprecated skills from source/skills/
// (In v2.0, commands and skills were unified — every command is a skill.)
function getCommandCount() {
const skillsDir = path.join(ROOT_DIR, 'source', 'skills');
const commandsDir = path.join(ROOT_DIR, 'source', 'commands');
if (!fs.existsSync(skillsDir)) return 0;
const skills = fs.existsSync(skillsDir)
? fs.readdirSync(skillsDir).filter(f => fs.statSync(path.join(skillsDir, f)).isDirectory()).length
: 0;
let count = 0;
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
if (!fs.existsSync(skillFile)) continue;
const commands = fs.existsSync(commandsDir)
? fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')).length
: 0;
const content = fs.readFileSync(skillFile, 'utf8');
const fm = content.match(/^---\n([\s\S]*?)\n---/);
if (!fm) continue;
return { skills, commands };
const frontmatter = fm[1];
const isUserInvocable = /^user-invocable:\s*true\s*$/m.test(frontmatter);
const isDeprecated = /^description:\s*["']?DEPRECATED/mi.test(frontmatter);
if (isUserInvocable && !isDeprecated) count++;
}
return count;
}
// 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 { skills, commands } = getCounts();
console.log(`Detected ${skills} skill(s), ${commands} command(s)`);
const commands = getCommandCount();
const extensionDataUrl = getExtensionDataUrl();
console.log(`Detected ${commands} command(s)`);
const html = `<!DOCTYPE html>
<html>
@@ -45,7 +69,7 @@ async function generateOgImage() {
<meta charset="UTF-8">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600;700&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Instrument+Sans:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -55,15 +79,20 @@ async function generateOgImage() {
overflow: hidden;
background: #f5f2ee;
position: relative;
font-family: 'Instrument Sans', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
color: #1a1a1a;
}
/* Subtle noise texture via radial gradient */
/* Soft brand glow for depth — subtle magenta accents in opposite corners */
body::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 30% 20%, rgba(200, 50, 120, 0.04) 0%, transparent 60%),
radial-gradient(ellipse at 80% 80%, rgba(200, 50, 120, 0.03) 0%, transparent 50%);
background:
radial-gradient(circle at 15% 8%, rgba(200, 50, 120, 0.07) 0%, transparent 55%),
radial-gradient(circle at 90% 95%, rgba(200, 50, 120, 0.05) 0%, transparent 60%);
pointer-events: none;
}
.container {
@@ -71,114 +100,128 @@ async function generateOgImage() {
width: 100%;
height: 100%;
padding: 72px 80px;
}
.content {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* Decorative slash */
.slash {
position: absolute;
right: 60px;
bottom: -40px;
font-family: 'Cormorant Garamond', Georgia, serif;
font-size: 500px;
font-weight: 300;
color: rgba(0, 0, 0, 0.04);
line-height: 1;
user-select: none;
height: 100%;
max-width: 560px;
z-index: 2;
}
.top {
display: flex;
flex-direction: column;
gap: 16px;
gap: 22px;
}
.title {
font-family: 'Cormorant Garamond', Georgia, serif;
font-size: 108px;
font-weight: 400;
font-weight: 300;
font-style: italic;
color: #1a1a1a;
letter-spacing: -0.02em;
line-height: 1;
letter-spacing: -0.03em;
line-height: 0.95;
}
.tagline {
font-family: 'Instrument Sans', system-ui, sans-serif;
font-size: 32px;
font-family: 'Cormorant Garamond', Georgia, serif;
font-size: 34px;
font-weight: 400;
color: #888;
margin-top: 4px;
font-style: italic;
color: #3a3a3a;
line-height: 1.3;
max-width: 480px;
}
.bottom {
display: flex;
flex-direction: column;
gap: 20px;
gap: 14px;
}
.stats {
.features {
display: flex;
align-items: center;
gap: 24px;
gap: 14px;
font-family: 'Space Grotesk', monospace;
font-size: 22px;
}
.stat {
display: flex;
align-items: center;
gap: 6px;
}
.stat-number {
font-weight: 600;
font-size: 19px;
font-weight: 500;
color: #1a1a1a;
letter-spacing: 0.005em;
}
.stat-label {
color: #888;
.feature-sep {
color: #c83278;
font-weight: 400;
}
.stat-sep {
color: #ccc;
font-size: 18px;
font-size: 22px;
line-height: 1;
}
.url {
font-family: 'Space Grotesk', monospace;
font-size: 18px;
color: #aaa;
font-size: 16px;
color: #999;
letter-spacing: 0.02em;
}
/* Extension panel — floating product shot, anchored right-center */
.panel {
position: absolute;
right: 54px;
top: 50%;
width: 500px;
transform: translateY(-50%) rotate(-2deg);
border-radius: 14px;
overflow: hidden;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.05),
0 6px 14px rgba(0, 0, 0, 0.06),
0 24px 48px -10px rgba(30, 15, 25, 0.18),
0 48px 100px -20px rgba(200, 50, 120, 0.14);
z-index: 1;
}
.panel::after {
content: '';
position: absolute;
inset: 0;
border-radius: 14px;
border: 1px solid rgba(0, 0, 0, 0.08);
pointer-events: none;
}
.panel img {
display: block;
width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="slash">/</div>
<div class="container">
<div class="top">
<div class="title">Impeccable</div>
<div class="tagline">Design fluency for AI harnesses</div>
</div>
<div class="bottom">
<div class="stats">
<div class="stat">
<span class="stat-number">${skills}</span>
<span class="stat-label">${skills === 1 ? 'skill' : 'skills'}</span>
</div>
<span class="stat-sep">+</span>
<div class="stat">
<span class="stat-number">${commands}</span>
<span class="stat-label">${commands === 1 ? 'command' : 'commands'}</span>
</div>
<span class="stat-sep">+</span>
<div class="stat">
<span class="stat-label">anti-patterns</span>
</div>
<div class="content">
<div class="top">
<div class="title">Impeccable</div>
<div class="tagline">Design fluency for AI harnesses</div>
</div>
<div class="url">impeccable.style</div>
<div class="bottom">
<div class="features">
<span>${commands} commands</span>
<span class="feature-sep">·</span>
<span>Chrome extension</span>
<span class="feature-sep">·</span>
<span>CLI</span>
</div>
<div class="url">impeccable.style</div>
</div>
</div>
<div class="panel">
<img src="${extensionDataUrl}" alt="Impeccable Chrome extension detection panel">
</div>
</div>
</body>
@@ -198,7 +241,7 @@ async function generateOgImage() {
await page.screenshot({
path: OUTPUT_PATH,
type: 'jpeg',
quality: 85,
quality: 90,
});
await browser.close();
@@ -207,7 +250,7 @@ async function generateOgImage() {
console.log(`Generated ${OUTPUT_PATH} (${size} KB)`);
}
generateOgImage().catch(err => {
generateOgImage().catch((err) => {
console.error('Failed to generate OG image:', err);
process.exit(1);
});