---
name: og-image-generator
description: Generate and optimize Open Graph meta images for social media sharing. Use this skill when building web applications that need dynamic OG image generation with support for Vercel's @vercel/og library, pre-generated image storage, and social media optimization (Twitter Cards, Facebook, LinkedIn). Handles dynamic routes, performance optimization, and includes best practices for crawler compatibility and testing.
---
# Open Graph Image Generator
Generate high-performance Open Graph images for social media sharing with support for dynamic generation and pre-generated storage patterns.
## When to use
Use this skill when:
- Building a web application that needs OG images for social sharing
- Creating meta images for articles, blog posts, or user-generated content
- Optimizing social media previews for Twitter Cards, Facebook, or LinkedIn
- Need both dynamic generation (few pages) and pre-generation (many pages) patterns
- Implementing performance-critical image generation with crawler timeout awareness
## Quick Start: Dynamic Generation (3 minutes)
### 1. Install dependency
```bash
npm install @vercel/og
```
### 2. Create API route (Next.js)
```typescript
import { ImageResponse } from '@vercel/og';
export async function GET(request: Request) {
return new ImageResponse(
(
Your Title
),
{ width: 2400, height: 1200 } // 2x scale for retina
);
}
```
### 3. Add meta tags
```html
```
### 4. Test
Visit: https://cards-dev.twitter.com/validator and paste your URL.
---
## Production Pattern: Pre-Generated Images
For user-generated content or high-traffic pages, pre-generate images when content is created to ensure instant crawler responses.
### Pre-generation at content creation
```typescript
import { ImageResponse } from '@vercel/og';
async function generateAndStoreOgImage(contentData) {
const image = new ImageResponse(
(
{contentData.title}
{contentData.description}
),
{ width: 2400, height: 1200 }
);
const buffer = await image.arrayBuffer();
// Store in database
await db.ogImages.insert({
contentId: contentData.id,
imageData: buffer,
mimeType: 'image/png',
});
return buffer;
}
```
### Serve pre-generated image
```typescript
export async function getOgImage(contentId: string) {
const { imageData } = await db.ogImages.findOne({ contentId });
return new Response(imageData, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400', // 24 hours
},
});
}
```
### Express pattern
```typescript
app.get('/og/:slug.png', async (req, res) => {
try {
const { slug } = req.params;
const content = await db.getContent(slug);
const image = new ImageResponse(
(/* JSX here */),
{ width: 2400, height: 1200 }
);
const buffer = await image.arrayBuffer();
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.send(Buffer.from(buffer));
} catch (error) {
res.status(500).send('Generation failed');
}
});
```
---
## Decision: Dynamic vs Pre-Generated
**Use Dynamic Generation if:**
- Few unique pages (<50)
- Content rarely changes
- Generation time < 2 seconds
- Traffic is low to medium
**Use Pre-Generation if:**
- Many unique pages (100+)
- User-generated content
- High traffic expected
- Need instant crawler response (avoid timeouts)
**Recommended:** Pre-generate for production; dynamic for development/testing.
---
## HTML Meta Tags
Include these in your page ``:
```html
```
**Important:** Include a zero-width space () in `og:title` to prevent crawlers from using default title when og:image is present.
---
## Design Best Practices
### Dimensions
- **Size:** 1200×600px (2:1 aspect ratio)
- **Rendering:** 2400×1200px (2x scale for retina quality)
- **Safe margins:** 50-56px on all sides
- **Format:** PNG (lossless, predictable)
### Visual Design
- **High contrast:** Text readable at small thumbnail sizes
- **Minimal text:** 3-5 words maximum for headline
- **Brand colors:** Consistent palette with your site
- **No complex gradients:** Use solid colors or simple patterns
### Performance
- **Generation time:** Target <3 seconds (crawlers timeout at ~5s)
- **File size:** Keep <200KB per image
- **Cache:** Set to 24 hours minimum (`Cache-Control: public, max-age=86400`)
---
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Twitter shows blank preview | Check: zero-width space in og:title, image is 1200×600, URL is public |
| Crawler timeout (X/Twitter) | Switch to pre-generated images; dynamic generation is too slow |
| Image doesn't load on social media | Verify CDN/storage URL is public; check CORS if cross-origin |
| Generation takes >5 seconds | Reduce complexity; pre-generate instead; use smaller images |
| File size >500KB | Reduce color palette; simplify shapes; avoid complex gradients |
| Meta tags not appearing | Ensure tags are in `` (not ``); use valid HTML |
---
## Validation Checklist
- [ ] Image dimensions: 1200×600px (or 2400×1200px for 2x)
- [ ] Aspect ratio: 2:1
- [ ] Safe margins: 50-56px padding
- [ ] High contrast colors (WCAG AA minimum)
- [ ] File format: PNG
- [ ] File size: <200KB
- [ ] Zero-width space in `og:title`
- [ ] `Cache-Control` header: `public, max-age=86400`
- [ ] Test with Twitter Card Validator: https://cards-dev.twitter.com/validator
- [ ] Response time <3 seconds (dynamic) or instant (pre-generated)
- [ ] Fallback error handling if generation fails
---
## Implementation Patterns
### Next.js with Dynamic Route
```typescript
// app/api/og/[slug]/route.ts
import { ImageResponse } from '@vercel/og';
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const content = await getContentBySlug(params.slug);
return new ImageResponse(
(
{content.title}
),
{ width: 2400, height: 1200 }
);
}
```
### Error Handling Pattern
```typescript
try {
const image = new ImageResponse(/* ... */);
return new Response(await image.arrayBuffer(), {
headers: { 'Content-Type': 'image/png' },
});
} catch (error) {
console.error('OG generation failed:', error);
// Return fallback or placeholder
return new Response('Image generation failed', { status: 500 });
}
```
### Hybrid Cache Pattern
```typescript
export async function getOgImage(contentId: string) {
// 1. Check cache
const cached = await cache.get(`og:${contentId}`);
if (cached) return cached;
// 2. Check if generating
if (isGenerating.has(contentId)) {
await waitFor(contentId);
return (await cache.get(`og:${contentId}`));
}
// 3. Generate with timeout
isGenerating.add(contentId);
try {
const buffer = await Promise.race([
generateOgImage(contentId),
timeout(4000), // Crawler safety
]);
await cache.set(`og:${contentId}`, buffer);
return buffer;
} finally {
isGenerating.delete(contentId);
}
}
```
---
## Common Variants
### Minimal Design
```typescript
const image = new ImageResponse(
(
{title}
),
{ width: 2400, height: 1200 }
);
```
### Card Style (Articles)
```typescript
const image = new ImageResponse(
(
{category}
{title}
{publishDate}
),
{ width: 2400, height: 1200 }
);
```
### With Image
```typescript
const image = new ImageResponse(
(
{title}
),
{ width: 2400, height: 1200 }
);
```
---
## Resources & Links
- **Vercel OG Documentation:** https://vercel.com/docs/og-image-generation
- **Twitter Card Validator:** https://cards-dev.twitter.com/validator
- **Open Graph Protocol:** https://ogp.me
- **Twitter Cards Docs:** https://developer.twitter.com/en/docs/twitter-for-websites/cards
---
## Summary
1. **For quick setup:** Use dynamic generation pattern above
2. **For production:** Pre-generate images at content creation
3. **For testing:** Use Twitter Card Validator
4. **For performance:** Monitor generation time; cache aggressively
5. **For crawlers:** Always be aware of 5-second timeout; pre-generation is safer
---
## Version
**v1.0** | MIT License | Production Ready