Generating OG Images
Overview
Generate branded 1200x630 OG images by rendering an HTML template with Playwright and screenshotting it. No image editor needed — design in HTML/CSS, capture as PNG.
Core Pattern
- HTML template — Self-contained 1200x630 page with CSS, placeholder variables (
{{title}}, {{gradient}}, etc.)
- Node.js script — Reads content source, injects variables, screenshots via Playwright
- Incremental mode — SHA-256 hash of template detects changes; only regenerates when needed
Quick Reference
| Component |
Convention |
| Viewport |
1200x630px (OG standard) |
| Template |
scripts/og-template.html |
| Script |
scripts/generate-og-images.js |
| Output |
public/img/og/ or static/images/og/ |
| Hash file |
.og-template-hash in output dir |
| Font |
System stack (no external fonts = no load delay) |
Template Design Rules
- Fixed 1200x630 body,
overflow: hidden
- Use
linear-gradient(135deg, ...) backgrounds for visual interest
- Decorative elements (circles, borders) at low opacity (
rgba(255,255,255,0.08-0.12))
- Title font size adapts to length:
<40 chars → 64px, 40-80 → 48px, >80 → 36px
- White text on gradient, author/subtitle at 70% opacity
- Badge/label for category/series with
rgba(255,255,255,0.2) background
Script Structure
const { chromium } = require('playwright');
const fs = require('fs');
const crypto = require('crypto');
// 1. Read template, compute hash
// 2. Compare hash to detect template changes
// 3. Find content items needing images
// 4. For each: inject variables → setContent → screenshot
// 5. Save hash file
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 1200, height: 630 });
// Variable injection via string replacement
const html = template
.replace(/\{\{title\}\}/g, escapedTitle)
.replace(/\{\{gradient\}\}/g, gradient);
await page.setContent(html, { waitUntil: 'load' });
await page.screenshot({ path: outputPath, type: 'png' });
For Single-Page Sites (no content scanning)
Skip the incremental/scanning logic. Just render one template:
const { chromium } = require('playwright');
const fs = require('fs');
const template = fs.readFileSync('scripts/og-template.html', 'utf-8');
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 1200, height: 630 });
await page.setContent(template, { waitUntil: 'load' });
await page.screenshot({ path: 'public/img/og-card.png', type: 'png' });
await browser.close();
Common Mistakes
| Mistake |
Fix |
| External fonts cause blank text |
Use system font stack |
| Title overflows card |
Adaptive font sizing by character count |
| Unescaped HTML in title |
Escape & → &, < → < |
| Regenerating unchanged images |
Hash template, skip if unchanged |
Missing waitUntil: 'load' |
Playwright may screenshot before CSS applies |
1---2name: generating-og-images3description: Use when a project needs Open Graph social card images generated from HTML templates via Playwright screenshot. Triggers include "OG image", "social card", "social preview image", "og:image", "twitter:image".4---56# Generating OG Images78## Overview910Generate branded 1200x630 OG images by rendering an HTML template with Playwright and screenshotting it. No image editor needed — design in HTML/CSS, capture as PNG.1112## Core Pattern13141. **HTML template** — Self-contained 1200x630 page with CSS, placeholder variables (`{{title}}`, `{{gradient}}`, etc.)152. **Node.js script** — Reads content source, injects variables, screenshots via Playwright163. **Incremental mode** — SHA-256 hash of template detects changes; only regenerates when needed1718## Quick Reference1920| Component | Convention |21|-----------|-----------|22| Viewport | 1200x630px (OG standard) |23| Template | `scripts/og-template.html` |24| Script | `scripts/generate-og-images.js` |25| Output | `public/img/og/` or `static/images/og/` |26| Hash file | `.og-template-hash` in output dir |27| Font | System stack (no external fonts = no load delay) |2829## Template Design Rules3031- Fixed 1200x630 body, `overflow: hidden`32- Use `linear-gradient(135deg, ...)` backgrounds for visual interest33- Decorative elements (circles, borders) at low opacity (`rgba(255,255,255,0.08-0.12)`)34- Title font size adapts to length: `<40 chars → 64px`, `40-80 → 48px`, `>80 → 36px`35- White text on gradient, author/subtitle at 70% opacity36- Badge/label for category/series with `rgba(255,255,255,0.2)` background3738## Script Structure3940```javascript41const { chromium } = require('playwright');42const fs = require('fs');43const crypto = require('crypto');4445// 1. Read template, compute hash46// 2. Compare hash to detect template changes47// 3. Find content items needing images48// 4. For each: inject variables → setContent → screenshot49// 5. Save hash file5051const browser = await chromium.launch();52const page = await browser.newPage();53await page.setViewportSize({ width: 1200, height: 630 });5455// Variable injection via string replacement56const html = template57 .replace(/\{\{title\}\}/g, escapedTitle)58 .replace(/\{\{gradient\}\}/g, gradient);5960await page.setContent(html, { waitUntil: 'load' });61await page.screenshot({ path: outputPath, type: 'png' });62```6364## For Single-Page Sites (no content scanning)6566Skip the incremental/scanning logic. Just render one template:6768```javascript69const { chromium } = require('playwright');70const fs = require('fs');7172const template = fs.readFileSync('scripts/og-template.html', 'utf-8');73const browser = await chromium.launch();74const page = await browser.newPage();75await page.setViewportSize({ width: 1200, height: 630 });76await page.setContent(template, { waitUntil: 'load' });77await page.screenshot({ path: 'public/img/og-card.png', type: 'png' });78await browser.close();79```8081## Common Mistakes8283| Mistake | Fix |84|---------|-----|85| External fonts cause blank text | Use system font stack |86| Title overflows card | Adaptive font sizing by character count |87| Unescaped HTML in title | Escape `&` → `&`, `<` → `<` |88| Regenerating unchanged images | Hash template, skip if unchanged |89| Missing `waitUntil: 'load'` | Playwright may screenshot before CSS applies |