mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
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:
co-authored by
Claude Opus 4.6
parent
27d1b13bc2
commit
2a38fad925
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 69 KiB |
+127
-84
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user