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.
Scanned 2/12/2026
Install via CLI
openskills install richtabor/agent-skills---
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.
No comments yet. Be the first to comment!