Open Graph images are the preview thumbnails shown when you share a URL on Twitter/X, LinkedIn, Slack, Discord, iMessage, and elsewhere. Getting them right can meaningfully increase click-through rates — studies consistently show that links with good preview images get 2–3× more clicks than bare URLs.
This guide covers everything: correct dimensions, formats, dynamic generation, caching strategies, debugging tools, and common mistakes that silently break your previews.
Correct dimensions
Universal safe size: 1200×630px
This is the standard. It works on Twitter/X, Facebook, LinkedIn, and most Slack/Discord unfurlers.
| Platform | Recommended | Min size | Aspect ratio |
|---|---|---|---|
| Twitter/X | 1200×630 | 600×314 | 1.91:1 |
| 1200×630 | 600×315 | 1.91:1 | |
| 1200×627 | 1200×627 | ~1.91:1 | |
| Discord | 1200×630 | Any | Crops to 1.91:1 |
| Slack | 1200×630 | 200×200 | Any (shown as-is) |
| iMessage | 1200×630 | 300×158 | 1.91:1 |
| 1200×630 | 300×200 | Letterboxed |
Stick to 1200×630 and you're safe everywhere. If you need to support only one platform, check that platform's current spec — they change occasionally.
Square images (1:1 ratio)
Some apps (WhatsApp, older Facebook mobile) show square thumbnails. If your OG image is square (e.g. 1200×1200), WhatsApp shows it full-size but Facebook crops it to 1.91:1. If you're optimizing for WhatsApp shares specifically, consider a 1:1 image with safe zone padding — keep your key content in the center 600×600px.
Format: JPEG or PNG?
Use JPEG for photos and complex gradients. Use PNG for sharp text and UI elements.
For generated OG images (templates), PNG is usually better — crisp text, predictable rendering, no JPEG artifacts on sharp edges.
Keep file size under 1MB. Most platforms cache OG images aggressively, but large files slow the initial unfurl. For JPEG: quality 80–85 is the sweet spot. For PNG: use a compressor like sharp with compression level 8–9.
WebP support
Most modern unfurlers support WebP, but some don't (particularly LinkedIn's crawler). Unless you control the full stack, stick to PNG or JPEG for OG images. WebP can save 20–40% file size, but a broken preview image is worse than a slightly larger one.
Required meta tags
Minimum required set:
<meta property="og:title" content="My Page Title" />
<meta property="og:description" content="A description of the page." />
<meta property="og:image" content="https://example.com/og-image.png" />
<meta property="og:url" content="https://example.com/page" />
<meta property="og:type" content="website" />Always include width and height:
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />Without them, some platforms re-fetch the image to determine dimensions, which adds latency to the unfurl and can cause the preview to appear without an image briefly.
For Twitter/X, add the Twitter Card tags:
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://example.com/og-image.png" />
<meta name="twitter:title" content="My Page Title" />
<meta name="twitter:description" content="A description." />Twitter/X uses the twitter:* tags when present, falling back to og:* if not. Use summary_large_image for the full-width banner format — summary only shows a small square thumbnail.
Common mistakes
1. Image behind authentication
Your OG image URL must be publicly accessible. Social crawlers don't have your session cookie. If your image URL returns 401 or 403, the platform shows a generic preview (or nothing).
Test with: curl -I https://your-domain.com/og-image.png — should return 200 OK without auth headers.
2. Wrong Content-Type header
Make sure your image URL serves Content-Type: image/jpeg or image/png. Some CDNs serve application/octet-stream by default — that breaks unfurlers.
For Next.js API routes that generate images dynamically:
return new Response(imageBuffer, {
headers: { 'Content-Type': 'image/png' },
});3. Missing og:image:width and og:image:height
Always include these meta tags. Without them, some platforms re-fetch to determine dimensions, adding latency and causing layout shift during unfurl.
4. Text too small
OG images are displayed at roughly 500px wide on most platforms. Use minimum 40px font size for titles on a 1200px canvas. If you're using a template, test at 50% zoom — if the text is hard to read, it's too small.
5. Not refreshing the cache
Facebook and LinkedIn cache OG images aggressively. After updating, force a refresh:
- Facebook: developers.facebook.com/tools/debug
- LinkedIn: linkedin.com/post-inspector
Twitter/X does not have a dedicated refresh tool — the cache typically expires in 7 days, or you can request re-scrape via the Card Validator.
6. Relative URLs
og:image must be an absolute URL including the protocol and domain. content="/og-image.png" will not work. Use content="https://example.com/og-image.png".
7. Using the same image for every page
For a blog, product pages, or user profiles, using a generic site-wide OG image misses the opportunity to drive clicks with contextual previews. Dynamic generation is worth the investment.
Static vs dynamic OG images
Static OG images are fine for marketing pages that don't change often. Store them in /public and point your meta tag there.
Dynamic OG images are necessary for:
- Blog posts (different title, date, author per post)
- Product pages (product name, price, image)
- User profiles (avatar, username)
- Documentation pages
- Any page where content changes
Three approaches to dynamic OG images
Option 1: @vercel/og
Vercel's Edge Runtime function. Easy to set up, good for simple layouts, supports JSX syntax. Limitations: only a subset of CSS, no box-shadow, no border-radius on images, and it's Vercel-specific.
// app/og/route.tsx (Next.js)
import { ImageResponse } from 'next/og';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title') ?? 'Untitled';
return new ImageResponse(
<div style={{ display: 'flex', fontSize: 48, background: '#000', color: '#fff', width: '100%', height: '100%', padding: 60 }}>
{title}
</div>,
{ width: 1200, height: 630 }
);
}Option 2: Puppeteer / Playwright
Full browser rendering — any CSS, custom fonts, pixel-perfect output. But complex to maintain: you need to run a headless browser process, handle memory leaks, scale it, deal with cold starts on serverless, and keep the Chromium binary updated.
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 1200, height: 630 });
await page.goto(`https://example.com/og-template?title=${encodeURIComponent(title)}`);
const screenshot = await page.screenshot({ type: 'png' });
await browser.close();This works but becomes a maintenance burden at scale.
Option 3: Screenshot API
Offload the browser work entirely. Render a template URL (or raw HTML) and get back a PNG — no infrastructure to manage.
curl -X POST "https://api.snapsharp.dev/v1/og-image" \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"template": "blog-post",
"data": {
"title": "My Post Title",
"author": "Jane Doe",
"date": "April 14, 2026"
}
}'Or generate from raw HTML:
import { SnapSharp } from '@snapsharp/sdk';
const snap = new SnapSharp('sk_live_...');
const image = await snap.screenshot({
html: `<div style="width:1200px;height:630px;background:#0a0a0b;display:flex;align-items:center;padding:80px">
<h1 style="color:#fff;font-size:60px;font-family:Inter,sans-serif">${title}</h1>
</div>`,
width: 1200,
height: 630,
});The result is cached in Redis for up to 1 hour, so repeated unfurls of the same URL don't trigger new renders.
Caching strategy
OG images are cached at multiple layers:
- Social platform cache (Facebook: days to weeks, LinkedIn: hours, Twitter: 7 days)
- Your CDN (Cloudflare, Vercel, etc.)
- Your server / screenshot API cache
Cache-busting after deploys
If you update a static OG image, append a version query string to the URL in your meta tag:
<meta property="og:image" content="https://example.com/og.png?v=2" />This forces social crawlers to re-fetch when they next scrape.
For dynamic images, use a cache TTL that matches your content update frequency. If blog posts rarely change, a 24-hour cache is fine. For live product prices, use a shorter TTL or bypass cache entirely.
SnapSharp cache control
const image = await snap.screenshot({
url: 'https://example.com/og-template?id=123',
cache: true,
cache_ttl: 86400, // 24 hours
});Pass cache: false when you need the freshest render (e.g. for breaking news or real-time data).
Debugging OG images
Before sharing a URL publicly, validate the preview:
-
Facebook Sharing Debugger — developers.facebook.com/tools/debug Shows exactly what Facebook's crawler sees, including the rendered preview, cache info, and any scraping errors. Use "Scrape Again" after updating.
-
LinkedIn Post Inspector — linkedin.com/post-inspector Shows the LinkedIn preview and lets you force a re-fetch.
-
Twitter Card Validator — cards-dev.twitter.com/validator Shows the Twitter card preview. Useful to verify
summary_large_imageis rendering correctly. -
OpenGraph.xyz — third-party tool that simulates previews across 10+ platforms simultaneously. Good for a quick multi-platform check.
-
curl / browser DevTools — check that your OG meta tags are in the HTML, that the image URL is accessible, and that the Content-Type is correct.
A/B testing OG images
You can run A/B tests on OG images by:
-
URL parameter routing: Serve different images based on a
variantquery param. Share both URL variants to different audiences and compare click rates. -
Time-based rotation: Rotate OG images weekly and track which periods had higher social engagement.
-
Platform-specific images: Detect the crawler's User-Agent (
facebookexternalhit,LinkedInBot,Twitterbot) and serve tailored images per platform.
// Next.js middleware or API route
const ua = request.headers.get('user-agent') ?? '';
if (ua.includes('Twitterbot')) {
return serveImage('twitter-variant.png');
} else if (ua.includes('LinkedInBot')) {
return serveImage('linkedin-variant.png');
}
return serveImage('default.png');The winning variant is usually: bold headline, high contrast, minimal text, brand logo in corner.
Checklist
Before shipping a page with OG images:
-
og:imageis an absolute URL - Image is 1200×630px
-
og:image:widthandog:image:heighttags present - Image is publicly accessible (no auth required)
- Content-Type is
image/pngorimage/jpeg - File size under 1MB
- Text is readable at 500px wide (40px+ font size)
-
twitter:cardset tosummary_large_image - Tested in Facebook Sharing Debugger
- Cache-busting strategy in place for future updates
Generating OG images at scale
Static OG images work for landing pages. For dynamic content — blog posts, user profiles, product pages — you need an automated generation pipeline.
SnapSharp provides templates and raw HTML rendering, so you can build once and generate for every piece of content:
# Generate OG for each blog post at build time
for slug in "${slugs[@]}"; do
curl -X POST "https://api.snapsharp.dev/v1/og-image" \
-H "Authorization: Bearer sk_live_..." \
-d "{\"template\":\"blog-post\",\"data\":{\"slug\":\"$slug\"}}" \
-o "public/og/$slug.png"
doneOr on-demand at request time, cached in Redis — no build-time overhead.
No infrastructure to manage. The image is cached in Redis for 1 hour by default.