Web & UI pack
Claude Skill

OG Image Generator

Generates Open Graph / social card images from title + author + brand. Templated via Satori or Puppeteer.

What it does

Produces 1200×630 OG images for social link previews (Twitter/X, LinkedIn, Slack, iMessage). Templated by title, author, optional subtitle, and brand. Two implementation paths: Satori (HTML→SVG→PNG, fast, runs at edge) or Puppeteer (HTML→screenshot, fully featured). Includes the API route to generate on demand.

When to use

  • You ship blog posts or marketing pages and need per-page OG images automatically
  • Existing OG image is a single static logo PNG — every link looks identical
  • You're using Next.js / Vercel and want \`@vercel/og\` or Satori OG generation

When not to use

  • You have a designer who hand-makes hero images per post — they'll do it better
  • You only ship 5 pages a year — design them in Figma, export, done

Install

Download the .zip, then unzip into your Claude skills folder.

mkdir -p ~/.claude/skills
unzip ~/Downloads/og-image-generator.zip -d ~/.claude/skills/

# Restart Claude Code session.
# Skill is now available — Claude will use it when relevant.

SKILL.md

SKILL.md
---
name: og-image-generator
description: Use when generating Open Graph / social card images from a template — title + author + brand. Triggers on "OG image", "social card", "open graph image", "Twitter card image".
---

# OG Image Generator

OG images are the link previews on Twitter/X, LinkedIn, Slack, iMessage, Discord. A bad one (or the same one for every page) is wasted real estate. Templated, per-page OG images compound link sharing.

## Required inputs

1. **Brand** — logo (SVG ideally), accent color, brand fonts (or Google Fonts you'll embed)
2. **Template variables** — what's dynamic per page (title, author, date, category, ...)
3. **Implementation** — Satori (Next.js / edge / Cloudflare Workers), Puppeteer (Node server), or one-off (Figma export)
4. **Output format** — PNG (universal). 1200x630 for Twitter Large Card and LinkedIn.

## Output format and dimensions

- **Standard: 1200x630px** — works for Twitter Large Card, LinkedIn, Open Graph default
- Twitter "summary" small card: 1200x600 also works
- iMessage: uses OG, fine with 1200x630
- 2:1 ratio is the safe default. Don't go square (Slack will crop weirdly).

## Path 1: Satori (recommended for static sites)

`@vercel/og` (Satori under the hood) — JSX → SVG → PNG. No headless browser, runs at edge.

```
// app/api/og/route.tsx
import { ImageResponse } from 'next/og';

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const title = searchParams.get('title') ?? 'Untitled';
  const author = searchParams.get('author') ?? '';

  return new ImageResponse(
    (
      <div style={{
        display: 'flex', flexDirection: 'column',
        width: '100%', height: '100%',
        background: 'linear-gradient(135deg, #4f46e5, #1e1b4b)',
        padding: 80, color: 'white',
      }}>
        <div style={{ fontSize: 64, fontWeight: 600, lineHeight: 1.1 }}>{title}</div>
        <div style={{ marginTop: 'auto', fontSize: 28, opacity: 0.85 }}>{author}</div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}
```

Constraints to know:
- Satori only supports a subset of CSS. **`display: flex` is required on every element with multiple children.** Block-level layout doesn't work as you'd expect.
- Custom fonts: load as ArrayBuffer and pass via `fonts` option. Google Fonts work via fetch.
- No `<img>` from arbitrary sources without explicit width/height
- No background-image gradients on inline elements; use `background` on a flex container

## Path 2: Puppeteer (fully featured)

Headless Chrome screenshot of an HTML page. Use this when you need full CSS, web fonts, complex layouts.

```
// pseudo
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630 });
await page.goto('https://yoursite.com/og-template?title=Foo');
const buffer = await page.screenshot({ type: 'png' });
```

Trade-offs:
- Slower (300-800ms per image)
- Heavier deploy (Chrome binary or `@sparticuz/chromium` for Lambda)
- BUT: full CSS support, web fonts, complex backgrounds

Cache aggressively — generate once per (title, author) tuple, store in R2 / S3 / Vercel Blob.

## Template structure

A solid OG layout, top to bottom or layered:

1. **Background** — solid brand color, gradient, or subtle pattern. Avoid busy photos.
2. **Brand mark** — small logo top-left or top-right (~48px height)
3. **Category / kicker** — small uppercase tag ("Engineering" / "Pricing")
4. **Title** — DOMINANT. `60-80px font-size`, semibold, line-height 1.05-1.15, max 3 lines, ellipsis after
5. **Meta row** — author avatar + name, date, read time (if blog)
6. **Footer mark** — domain ("yoursite.com") bottom-right

Long titles: use `text-overflow: ellipsis` after 3 lines, OR scale font down dynamically based on length (more work, prettier).

## Style guidance

- Use ONE display font, semibold or bold. Inter, Manrope, Geist, Cal Sans all read well at 60px.
- High contrast: white on dark accent, or very dark on light background. Avoid medium-on-medium.
- Avatar: circle, 64-80px, with `border: 2px solid white` for separation
- Don't use brand-cluttered backgrounds. The title should dominate.
- Test on Twitter / Slack at small preview sizes — if title is unreadable, font is too small

## Hooking it up

In your page `<head>`:

```
<meta property="og:image" content="https://yoursite.com/api/og?title=My+Post&author=Jane">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="My Post — by Jane">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://yoursite.com/api/og?title=...">
```

Always include explicit `width`, `height`, and `alt` on the og:image meta — Slack and LinkedIn use them.

## Caching

- For dynamic params: hash them, cache the PNG to blob storage with a long TTL
- Add `Cache-Control: public, max-age=31536000, immutable` if the URL hashes the inputs
- Vercel `@vercel/og` caches at the edge automatically

## Accessibility

OG images are accessibility-adjacent: screen readers don't read them, but social platforms display the `alt` text in some contexts. Always set `og:image:alt`.

If the OG image is the only place a piece of info appears (e.g., a chart's value), reproduce it in body text — the image is decorative as far as a11y is concerned.

## Anti-patterns

- **One static OG image for the whole site**: massive missed opportunity
- **Title cut off at 1 line**: most posts have titles > 8 words. Plan for 2-3 lines.
- **Tiny title with huge brand logo**: invert. Title dominates.
- **Photo background with text on top, low contrast**: unreadable in feeds
- **Forgetting og:image:width/height**: some platforms refuse the image
- **Generating on every request**: cache. Edge or blob.
- **Wrong aspect ratio**: 1200x630 is the standard. Square gets cropped.

## Output

Return the API route + template + meta tag snippet. Top comment lists:
- Implementation chosen (Satori vs Puppeteer)
- Variables supported
- Caching strategy
- Recommended cache key
- A11y note (alt text via og:image:alt)

Example prompts

Once installed, try these prompts in Claude:

  • Build an OG image template using @vercel/og: post title, author name + avatar, our brand color, gradient bg.
  • Generate a Puppeteer-based OG image API. HTML template + screenshot at 1200x630. Cache to Cloudflare R2.