og-images

$npx mdskill add richtabor/agent-skills/og-images

Generate social media images for OpenGraph and Twitter previews.

  • Creates dynamic 1200x630 or 1200x600 images for social sharing.
  • Integrates with Next.js ImageResponse and Satori rendering engine.
  • Selects file-based or API route based on content staticness.
  • Delivers styled images with custom fonts, avatars, and layout patterns.
SKILL.md
.github/skills/og-imagesView on GitHub ↗
---
name: og-images
description: Guides creation of OpenGraph and Twitter share images using next/og ImageResponse. Covers layout patterns, custom fonts, avatars, title case, and Satori rules. Use when building OG images, Twitter cards, or social previews.
---

# Creating Share Images for Next.js

Generate dynamic OpenGraph (1200x630) and Twitter (1200x600) images using `next/og` ImageResponse.

## Choosing an Approach

- **File-based route** (`app/opengraph-image.tsx`): Best for static pages with known titles at build time. Export `runtime`, `alt`, `size`, `contentType`, and a default `Image` function.
- **API route** (`app/api/og/route.tsx`): Best for dynamic content (blog posts, CMS). Accept `slug` and/or `title` as query params. Reference in metadata via `generateMetadata()`.

Use `export const runtime = "edge"` for both approaches.

## File Naming Convention

| File | Purpose | Dimensions |
|------|---------|------------|
| `opengraph-image.tsx` | Facebook, LinkedIn, iMessage | 1200x630 |
| `twitter-image.tsx` | Twitter/X cards | 1200x600 |
| `app/api/og/route.tsx` | Dynamic API route | 1200x630 |

Place file-based routes in the relevant route directory (e.g., `app/about/opengraph-image.tsx` for `/about`).

## Layout Pattern

- Use `flexDirection: "column"` with `justifyContent: "space-between"` to separate content from branding
- Title and subtitle (e.g., author name) go top-left in a stacked flex column
- Title: large, bold/medium weight, dark color
- Subtitle: same size or smaller, lighter weight, muted color (e.g., `#888`)
- Keep text left-aligned with `textWrap: "balance"` and constrain width with `maxWidth`
- Use `letterSpacing: "-0.02em"` for tight, editorial feel at large sizes
- Padding: `48px` on all sides works well at 1200x630

## Avatar / Logo (Optional)

If the project has an avatar or logo, place it in the bottom-right corner using a flex container with `justifyContent: "flex-end"`. Load images via `fetch` + `arrayBuffer`, convert to base64 data URI for the `src`. Use `borderRadius: "50%"` for circular avatars. Cache loaded assets in a `Map` to avoid refetching.

## Custom Fonts

Load `.ttf` files from `public/fonts/` using `new URL("../../../public/fonts/YourFont.ttf", import.meta.url)`. Pass the `ArrayBuffer` to `ImageResponse` via the `fonts` option. Cache the font buffer after first load. Match the `weight` in the fonts config to the actual font file weight.

## Title Case

If titles come from a CMS, apply smart title case:
- Lowercase small words (a, an, the, and, but, for, in, of, etc.) unless first or last
- Always capitalize brand names correctly (WordPress, JavaScript, GitHub, macOS, etc.)
- Uppercase known acronyms (AI, API, CSS, HTML, UI, UX)
- Handle hyphenated words by capitalizing each part independently

## Metadata Integration

Reference the OG route in `generateMetadata()`:

```tsx
export function generateMetadata({ params }) {
  return {
    openGraph: {
      images: [`/api/og?slug=${params.slug}`],
    },
  };
}
```

For static pages, pass the title directly: `/api/og?title=About`.

## Satori Rules

These are hard requirements of the `next/og` rendering engine (Satori):

1. **Every element needs `display: "flex"`** — this is the only layout mode
2. **Inline styles only** — no CSS classes, no external stylesheets, no CSS variables
3. **All text must be in elements** with explicit style props
4. **Use hex colors** — no `rgb()`, `hsl()`, or CSS variables
5. **No `gap` on older versions** — test before relying on it; fallback to margin

## Common Issues

| Issue | Solution |
|-------|----------|
| Text not rendering | Add `display: "flex"` to the text wrapper |
| Layout broken | Ensure all containers have `display: "flex"` |
| Colors wrong | Use hex colors, not CSS variables |
| Font not loading | Check the relative path from route file to `public/fonts/` |
| Image not showing | Convert to base64 data URI, don't use relative paths |

## Testing

Preview during development by visiting the route directly in the browser:

```
http://localhost:3000/api/og?title=Hello+World
```

After building, verify routes register as dynamic (`f` prefix) in the build output.
More from richtabor/agent-skills