mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Replace static OG image with auto-generated JPEG using site fonts
Add scripts/generate-og-image.js that renders the OG image with Playwright using the actual site fonts (Cormorant Garamond, Instrument Sans, Space Grotesk) and dynamically counts skills/commands from source/. Switch from PNG to JPEG (512 KB → 25 KB). Run with `bun run og-image`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
98f5d839fd
commit
a94e237359
+2
-1
@@ -24,7 +24,8 @@
|
||||
"dev": "bun run server/index.js",
|
||||
"start": "bun --production run server/index.js",
|
||||
"test": "bun test",
|
||||
"screenshot": "bun run scripts/screenshot-antipatterns.js"
|
||||
"screenshot": "bun run scripts/screenshot-antipatterns.js",
|
||||
"og-image": "bun run scripts/generate-og-image.js"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@
|
||||
<meta property="og:url" content="https://impeccable.style">
|
||||
<meta property="og:title" content="Impeccable: Design skills for AI coding tools">
|
||||
<meta property="og:description" content="1 skill, 17 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
|
||||
<meta property="og:image" content="https://impeccable.style/og-image.png">
|
||||
<meta property="og:image" content="https://impeccable.style/og-image.jpg">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
@@ -21,7 +21,7 @@
|
||||
<meta name="twitter:creator" content="@pbakaus">
|
||||
<meta name="twitter:title" content="Impeccable: Design skills for AI coding tools">
|
||||
<meta name="twitter:description" content="1 skill, 17 commands, and curated anti-patterns for impeccable frontend design.">
|
||||
<meta name="twitter:image" content="https://impeccable.style/og-image.png">
|
||||
<meta name="twitter:image" content="https://impeccable.style/og-image.jpg">
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
|
||||
<link rel="apple-touch-icon" href="./apple-touch-icon.png">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 71 KiB |
@@ -1,32 +0,0 @@
|
||||
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f5f3ef"/>
|
||||
<stop offset="100%" style="stop-color:#e8e4df"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||
|
||||
<!-- Decorative slash -->
|
||||
<text x="950" y="480" font-family="system-ui, -apple-system, sans-serif" font-size="400" font-weight="300" fill="#1a1a1a" opacity="0.06">/</text>
|
||||
|
||||
<!-- Main title -->
|
||||
<text x="80" y="280" font-family="Georgia, 'Times New Roman', serif" font-size="96" font-weight="400" fill="#1a1a1a">Impeccable</text>
|
||||
|
||||
<!-- Tagline -->
|
||||
<text x="80" y="360" font-family="system-ui, -apple-system, sans-serif" font-size="32" fill="#6b7280">Design skills for AI coding tools</text>
|
||||
|
||||
<!-- Stats -->
|
||||
<text x="80" y="460" font-family="ui-monospace, monospace" font-size="24" fill="#1a1a1a">
|
||||
<tspan fill="#1a1a1a" font-weight="500">1</tspan><tspan fill="#6b7280"> skill</tspan>
|
||||
<tspan fill="#6b7280" dx="20">+</tspan>
|
||||
<tspan fill="#1a1a1a" font-weight="500" dx="20">17</tspan><tspan fill="#6b7280"> commands</tspan>
|
||||
<tspan fill="#6b7280" dx="20">+</tspan>
|
||||
<tspan fill="#6b7280" dx="20">anti-patterns</tspan>
|
||||
</text>
|
||||
|
||||
<!-- URL -->
|
||||
<text x="80" y="560" font-family="ui-monospace, monospace" font-size="20" fill="#9ca3af">impeccable.style</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
+1
-1
@@ -137,7 +137,7 @@ async function build() {
|
||||
await buildStaticSite();
|
||||
|
||||
// Copy root-level static assets that need stable (unhashed) URLs
|
||||
const staticAssets = ['og-image.png', 'robots.txt', 'sitemap.xml', 'favicon.svg', 'apple-touch-icon.png'];
|
||||
const staticAssets = ['og-image.jpg', 'robots.txt', 'sitemap.xml', 'favicon.svg', 'apple-touch-icon.png'];
|
||||
const buildDir = path.join(ROOT_DIR, 'build');
|
||||
for (const asset of staticAssets) {
|
||||
const src = path.join(ROOT_DIR, 'public', asset);
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate OG Image
|
||||
*
|
||||
* Renders the OG image using Playwright with proper Google Fonts.
|
||||
* Counts skills and commands dynamically from the source/ directory.
|
||||
*
|
||||
* Usage: bun run og-image
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } 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, 'public', 'og-image.jpg');
|
||||
|
||||
// Count skills and commands from source directory
|
||||
function getCounts() {
|
||||
const skillsDir = path.join(ROOT_DIR, 'source', 'skills');
|
||||
const commandsDir = path.join(ROOT_DIR, 'source', 'commands');
|
||||
|
||||
const skills = fs.existsSync(skillsDir)
|
||||
? fs.readdirSync(skillsDir).filter(f => fs.statSync(path.join(skillsDir, f)).isDirectory()).length
|
||||
: 0;
|
||||
|
||||
const commands = fs.existsSync(commandsDir)
|
||||
? fs.readdirSync(commandsDir).filter(f => f.endsWith('.md')).length
|
||||
: 0;
|
||||
|
||||
return { skills, commands };
|
||||
}
|
||||
|
||||
async function generateOgImage() {
|
||||
const { skills, commands } = getCounts();
|
||||
console.log(`Detected ${skills} skill(s), ${commands} command(s)`);
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
width: 1200px;
|
||||
height: 630px;
|
||||
overflow: hidden;
|
||||
background: #f5f2ee;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Subtle noise texture via radial gradient */
|
||||
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%);
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 72px 80px;
|
||||
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;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'Cormorant Garamond', Georgia, serif;
|
||||
font-size: 108px;
|
||||
font-weight: 400;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-family: 'Instrument Sans', system-ui, sans-serif;
|
||||
font-size: 32px;
|
||||
font-weight: 400;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
font-family: 'Space Grotesk', monospace;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #888;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.stat-sep {
|
||||
color: #ccc;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.url {
|
||||
font-family: 'Space Grotesk', monospace;
|
||||
font-size: 18px;
|
||||
color: #aaa;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
</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 coding tools</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>
|
||||
<div class="url">impeccable.style</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1200, height: 630 },
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
|
||||
await page.setContent(html, { waitUntil: 'networkidle' });
|
||||
|
||||
// Wait for fonts to load
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
|
||||
await page.screenshot({
|
||||
path: OUTPUT_PATH,
|
||||
type: 'jpeg',
|
||||
quality: 85,
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user