How to add Open Graph images that don't look broken
You shared your launch link and the preview was a grey box, a blurry logo, or a screenshot with the headline cut in half. That's not a design problem — it's four specific, fixable causes. This guide covers all of them: the right size, the right tags, dynamic generation in Next.js, and the scraper cache that makes your fix look like it didn't work.
The 30-second version
Export one image at 1200 × 630px. Put it at a public, absolute URL (https://yoursite.com/og.jpg — never /og.jpg). Add the full tag set below, including twitter:card. Then force a re-scrape, because X and Facebook cache the old broken preview for days. Skip any one of those four and the share still looks broken.
01Why previews look broken
When someone pastes your URL into X, LinkedIn, Slack, Discord, iMessage or Facebook, that platform sends a bot (a “scraper”) to your page, reads the Open Graph tags in your <head>, and builds the preview card from them. It never renders your page like a browser does. Every broken preview traces back to one of these:
No og:image at all. The scraper has nothing to show, so you get a plain text link or a grey placeholder box. This is the default state of most vibe-coded apps — AI builders rarely add OG tags unless asked.
A relative or unreachable URL.og:image" content="/og.jpg" or a localhost URL looks fine in your browser's view-source and fails completely for a scraper on another machine. Scrapers require absolute URLs, publicly reachable, with no login wall or redirect chain in the way.
Wrong size or a screenshot. A 400×300 screenshot, a square logo, or a full-page capture gets center-cropped into a 1.91:1 frame — headlines sliced, text at unreadable sizes, logos stretched blurry.
The cache. Platforms cache the first scrape aggressively. You fix everything, re-share, and see the old broken card — so you assume the fix failed. It didn't; the cache needs busting (section 7).
no og:image → grey box / plain link
No image tag
The launch post nobody clicks. Most common, easiest fix.
LOGO · 200×200, upscaled
Too small, wrong ratio
A square logo stretched into a wide frame turns to mush.
My Amazing New App Lau…
Raw screenshot
Full-page capture center-cropped: headline cut, UI text microscopic.
02The spec: one size that works everywhere
Every major platform converged on the same canvas. Design for it once and you're done:
1200 × 630pixels — a 1.91:1 ratio. The size Facebook recommends and X, LinkedIn and Slack all render cleanly.
PNG or JPGBoth fine. Avoid SVG (most scrapers won't render it) and avoid WebP if you want maximum compatibility.
Under 1 MBAim for 200–500 KB. Some scrapers time out or skip oversized images; 5 MB heroes silently fail.
Center 76%is the safe zone. Some surfaces crop to squarer ratios — keep text and logos out of the edges.
Design it, don't screenshot it. A good OG image is a billboard, not a UI capture: your product name, one line of benefit, maybe one clean visual. Text should be huge — assume it renders at ~500px wide in a feed, so anything under ~40px in the 1200px-wide file is decoration, not information. This page's own OG image (the one that renders when you share this guide) follows exactly that recipe:
This guide's actual og:image — 1200 × 630, one headline, one supporting line, nothing near the edges. Steal the layout.
SAFE ZONE
Toggle the safe zone, then try the crops platforms apply on some surfaces:
Notice what the square crop does: everything near the left and right edges is sliced away, and only the center carries the message. That's exactly why the safe zone exists — if your product name or headline lived in a corner, it would be gone entirely.
03See it: how each platform renders the same card
Same tags, four slightly different renderings. Switch platforms to see what your share actually looks like — this is the specimen above, presented the way each feed presents it.
S
ShipReady@shipready · Launch day
We shipped! The guide to Open Graph images that don't look broken — the exact fix for grey-box link previews 👇
shipready guideHow to Add Open Graph Images That Don't Look Broken1200×630, absolute URLs, the full tag set, Next.js dynamic generation, and how to beat the scraper cache.
X uses twitter:card = summary_large_image; without that tag it falls back to a small square thumbnail even if og:image exists.
04The fix: the full meta tag set
Most “add one tag” advice is why previews still break — X wants its own card tag, scrapers want declared dimensions, and everything wants absolute URLs. Paste this complete set into your <head> and replace the values:
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://yoursite.com/this-page" />
<meta property="og:title" content="Your App — the one-line benefit" />
<meta property="og:description" content="One clear sentence about this page." />
<meta property="og:image" content="https://yoursite.com/og.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Your App — main feature" />
<!-- X / Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Your App — the one-line benefit" />
<meta name="twitter:description" content="One clear sentence about what this page does." />
<meta name="twitter:image" content="https://yoursite.com/og.jpg" />
Every URL is absolute — https:// included. This single rule is the difference between “works in view-source” and “works on X.”
og:image:width/height let some platforms reserve the right box before the image downloads, instead of rendering a collapsed card first.
twitter:card is not optional if you care about X. Without it, X may use the small summary format — a tiny square thumbnail — even with a perfect og:image.
One image per page is fine. Different pages (pricing, blog posts, docs) can each have their own; the homepage one matters most because it's what your launch post uses.
The image must load without JavaScript, cookies, or a redirect to a login page. Scrapers don't log in and barely follow redirects.
The vibe-coder trap
Ask your AI builder to “add an OG image” and it will often generate a lovely image, save it in the project, and reference it as /og.jpg or — worse — wire it only into a React component instead of the static <head>. If your page is client-rendered, check that the tags are in the served HTML (view-source, not DevTools' Elements panel). Some scrapers run JavaScript poorly or not at all.
05Next.js: two good ways, one classic mistake
Way 1 — A static image file (simplest, do this first)
In the App Router, drop a file named opengraph-image.jpg (or .png) into your app/ folder — or into any route segment to give that section its own image — and Next.js generates the tags for you. Add titles and descriptions through the Metadata API:
// app/layout.tsx
export const metadata = {
metadataBase: new URL("https://yoursite.com"),
title: "Your App — the one-line benefit",
description: "One clear sentence about what this page does.",
openGraph: {
title: "Your App — the one-line benefit",
description: "One clear sentence about what this page does.",
url: "https://yoursite.com",
siteName: "Your App",
type: "website",
},
twitter: { card: "summary_large_image" },
};
// + app/opengraph-image.jpg (1200 × 630)
// Next.js serves it and emits og:image + twitter:image automatically.
The classic mistake is skipping metadataBase. Without it, Next.js can't turn your image path into the absolute URL scrapers require — you get a build warning, a relative URL in production, and a broken preview that “worked in dev.” Set metadataBase once in the root layout and every relative OG path resolves correctly.
Way 2 — Dynamic images with opengraph-image.tsx
When every page or post needs its own card (blog posts, user profiles, generated content), render the image in code with next/og. Replace the static file with a component:
It's CSS-flexbox only — ImageResponse uses Satori under the hood: every element needs display: "flex" (or none), no grid, no external CSS. When your design renders blank, a missing display: flex is usually why.
Custom fonts must be loaded as data (read the font file and pass it in the options) — the default font is the only one available out of the box.
Per-route versions: put an opengraph-image.tsx inside app/blog/[slug]/ and read the post title from params to generate a unique card per post. That's how the big content sites get a fresh, relevant card on every share.
Cache it. Dynamic images are generated on request; on most hosts they're cached at the edge after the first hit, but a cold, slow generation can make a scraper give up. Keep the component light.
On other stacks the pattern is identical: Astro takes the same tags in your layout's <head>; plain Vite/React apps put them in index.html; for client-rendered SPAs, make sure the tags exist in the initial HTML or use prerendering/SSR for shared routes — scrapers and SPA shells are old enemies.
06Build your tags — and check the ones you have
Fill in four fields and get the exact block to paste. Your answers also update nothing on any server — this runs in your browser.
OG tag builder
Already have tags? Paste your <head> and check them
Copy from view-source (not a browser extension) and paste below. We check for the exact failure modes from section 1.
07“I fixed it and it still shows the old image”
Welcome to the scraper cache — the reason half of all “OG doesn't work” threads exist. Platforms fetch your tags once and reuse the result: Facebook holds it until you force a re-scrape, X caches for roughly a week, LinkedIn until you re-inspect. The fix ritual, in order:
Verify the live tags first. Open your page, view-source, and find the og:image line. Click that URL directly — it must open the image, publicly, with no login. If it 404s or redirects to your homepage, no amount of cache-busting helps.
Force a re-scrape: Facebook/Meta → Sharing Debugger (search “Facebook sharing debugger”), paste the URL, hit Scrape Again. LinkedIn → Post Inspector. For X there's no official tool — the cache clears on its own (often within days), or you can share the URL with a throwaway query string to test the new card.
Bust it permanently with a versioned filename. Change og.jpg to og-v2.jpg (or add ?v=2 — most scrapers treat it as a new image). Every redesign gets a new version; never fight the same cache twice.
Check for two competing tag sets. SEO plugins, frameworks and AI builders love to each emit their own OG tags. If view-source shows og:image twice, scrapers pick one — not necessarily yours. Keep exactly one set.
Give it one fetch. A brand-new image URL can take a moment on first scrape while the platform downloads and processes a large file — another reason to stay under 1 MB.
Test before you launch, not after
Paste your URL into a private Slack channel or a draft post and look at the card before the launch tweet. The preview you see there is the preview everyone gets — and catching a grey box in a draft costs you thirty seconds instead of your launch.
08Pre-share checklist
✓
Image exists at 1200 × 630PNG or JPG, under 1 MB, designed — not a page screenshot.
✓
Key content sits in the center safe zoneNothing important within ~10% of any edge; headline readable at 500px wide.
✓
Full tag set in the served HTMLog:type, og:url, og:title, og:description, og:image with width/height/alt — visible in view-source.
✓
Every URL is absolute and publichttps:// included; the image URL opens in a private window with no login.
✓
twitter:card = summary_large_imagePlus twitter:title / description / image — X doesn't fully fall back to OG.
✓
Next.js: metadataBase is setWithout it, production emits relative URLs and previews break only after deploy.
✓
Only one OG tag set on the pageNo framework + plugin + hand-written duplicates fighting each other.
✓
Re-scraped after the fixSharing Debugger / Post Inspector run, or image filename version bumped.
✓
Preview eyeballed in a draft postPrivate Slack message or draft tweet shows the real card before launch.
That's one check. Your launch has about nine more.
The missing OG image is the most visible ShipReady finding — but it's rarely the only one. The same scan that flags it also catches a default “Vite App” title, a missing favicon and viewport, console.logs in your production bundle, and — once you verify the site is yours — leaked API keys, open Supabase tables, and unprotected API routes.
Scan your live site free →
Free scan · paste your URL, no code upload · secret findings unlock only after you prove the site is yours.