Sitemd og-image
Generate a custom branded OG social share image from the site's theme colors and content.
git clone https://github.com/sitemd-cc/sitemd
T=$(mktemp -d) && git clone --depth=1 https://github.com/sitemd-cc/sitemd "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/og-image" ~/.claude/skills/sitemd-cc-sitemd-og-image-34b7ca && rm -rf "$T"
.claude/skills/og-image/SKILL.mdOG Image Generator
Generate a branded social share image (1200×630 @2x) by writing and running a Node script using Satori + Resvg. The image is saved to
media/og-image.png and configured in SEO settings.
No permission required — execute immediately.
Procedure
Step 1: Read site config
Read these settings files in parallel:
— get brand name, description, brand imagesettings/meta.md
— getsettings/theme.mddefaultMode
— check current OG image statesettings/seo.md
Step 2: Determine theme colors
Check
defaultMode from theme settings:
- If
,dark
, orlight
→ use that modepaper - If
→ ask the user: "Your theme is set to system mode. Which theme should the OG image use: dark, light, or paper?"system
Read
theme/styles.css to extract CSS variables for the chosen mode:
- Light mode:
block:root, [data-theme="light"] - Dark/paper:
block[data-theme="<mode>"] - Extract:
,--color-bg
,--color-bg-secondary
,--color-bg-tertiary
,--color-text
,--color-text-secondary
,--color-text-tertiary
,--color-border
,--color-accent
,--color-code-bg--color-code-text
Step 3: Ask the user 3 questions
Present smart defaults based on the config:
- Heading text — default: brand name or site title. "What heading should appear on the image?"
- Subheading text — default: site description. "What subheading (if any)?"
- Preview object — "What should the main visual be? This is what people see when the link is shared. Could be a product screenshot, a browser mockup, a code snippet, an illustration, a logo — whatever represents your site." If the user doesn't want a preview object, use a simpler layout (heading + subheading + optional brand image). If they do want one, design and build it with Satori elements.
If the user gives short answers or says "defaults are fine", use the defaults.
Step 4: Write and run a generation script
Write a standalone Node script (e.g.
scripts/generate-og.js) that generates the image. Run it with node. Delete the script after successful generation.
Script template
const fs = require('fs'); const path = require('path'); // JSX-like element helper for Satori function h(type, props, ...children) { const flat = children.flat().filter(Boolean); return { type, props: { ...props, children: flat.length === 1 ? flat[0] : flat.length === 0 ? undefined : flat } }; } (async () => { const satori = (await import('satori')).default; const { Resvg } = require('@resvg/resvg-js'); // Load Inter fonts from engine const fontsDir = path.join(__dirname, '..', 'sitemd', 'engine', 'seo', 'fonts'); const fonts = [ { name: 'Inter', data: fs.readFileSync(path.join(fontsDir, 'Inter-Regular.ttf')), weight: 400, style: 'normal' }, { name: 'Inter', data: fs.readFileSync(path.join(fontsDir, 'Inter-Bold.ttf')), weight: 700, style: 'normal' }, ]; // Build the layout using h() — design freely based on user's request const element = h('div', { style: { width: '100%', height: '100%', display: 'flex', // ... your layout here, using the theme colors }, }, /* child elements */); // Render: Satori → SVG, Resvg → PNG at 2x const svg = await satori(element, { width: 1200, height: 630, fonts }); const resvg = new Resvg(svg, { fitTo: { mode: 'width', value: 2400 } }); const png = resvg.render().asPng(); const outPath = path.join(__dirname, '..', 'media', 'og-image.png'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, png); console.log(`Generated ${outPath} (${(png.length / 1024).toFixed(0)} KB)`); })();
Satori constraints
Satori renders a subset of CSS via flexbox. Key rules:
- Every element must use
(no block/grid/inline)display: 'flex' - Use
,flexDirection
,alignItems
for all layoutjustifyContent
— the loaded font. UsefontFamily: 'Inter'
for boldfontWeight: 700- Supports:
,position
,border
,borderRadius
,background
(linear/radial gradients),backgroundImage
,boxShadow
,opacity
,overflow: 'hidden'gap - Does NOT support: CSS grid,
(except on the root),transform
,filter
,backdrop-filter
,animation@font-face - Images: use
elements with<img>
as data URIs or absolute URLssrc - Text must be direct children — wrap in a
or<div>
, not bare strings mixed with elements<span>
Safe zones and padding
The canvas is 1200×630 (rendered at 2400×1260 for retina). Social platforms crop unpredictably:
- Use 60-80px padding on all sides as a safe zone
- Keep critical content (text, logos) within the inner ~1060×470 area
- Full-bleed background colors and subtle gradients can extend to edges
- Place accent bars, borders, and decorative elements at edges — they survive partial cropping
Step 5: Update SEO settings
After generating the image:
- Update
— setsettings/seo.mdogImage: /media/og-image.png
Step 6: Show the result
Tell the user:
- "Generated
— open the file to preview it."media/og-image.png - "SEO settings updated to use this image."
- "Want any changes? I can adjust the text, colors, or layout and regenerate."
Step 7: Iterate
If the user wants changes, update the script and regenerate. They can review the raw PNG each time without restarting the dev server.
Rules
- Always read site config before asking questions — use real values as defaults
- Never generate without asking the user first (unless they explicitly provide all parameters)
- Save to
media/og-image.png - Clean up the generation script after successful runs
- Design freely — there are no fixed layouts. Build whatever best represents the user's site
- Keep iteration fast — modify and re-run the script, no server restart needed