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 comes down to 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:

  1. 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, because AI builders rarely add OG tags unless asked.
  2. 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.
  3. 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, which slices headlines, shrinks text past reading and blurs logos.
  4. 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 to 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, so keep text and logos away from 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 about 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 own Open Graph image: dark frame, cream card, the headline 'Open Graph images that don't look broken', a line reading '1200 by 630, absolute URL, designed not screenshotted', and a 'Free fix guide' pill.
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:
Crop preview of the guide's OG image at the selected aspect ratio

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 why the safe zone exists. If your product name or headline sat in a corner, it would be gone.

03See it: how each platform renders the same card

Same tags, four slightly different renderings. Switch platforms to see how your share will look. This is the image above, shown the way each feed shows it.

S
ShipReady@shipready · Launch day

We shipped! The guide to Open Graph images that don't look broken, with the exact fix for grey-box link previews 👇

Preview card image
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" />
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:

// app/opengraph-image.tsx
import { ImageResponse } from "next/og";

export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export const alt = "Your App: the one-line benefit";

export default function OgImage() {
  return new ImageResponse(
    (
      <div style={{
        width: "100%", height: "100%", display: "flex",
        flexDirection: "column", justifyContent: "center",
        background: "#16130E", color: "#FFF8EE", padding: 80,
      }}>
        <div style={{ fontSize: 68, fontWeight: 800, lineHeight: 1.1 }}>
          Your App
        </div>
        <div style={{ fontSize: 34, color: "#FF4D1C", marginTop: 16 }}>
          The one-line benefit, big and centered
        </div>
      </div>
    ),
    { ...size }
  );
}

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 can't run your JavaScript.

06Build your tags and check the ones you have

Fill in four fields and get the exact block to paste. It all runs in your browser, and nothing is sent to a server.

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”

This is the scraper cache, the reason so many "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:

  1. Verify the live tags first. Open your page, view-source, and find the og:image line. Open that URL directly. It must show the image publicly, with no login. If it 404s or redirects to your homepage, no amount of cache-busting helps.
  2. Force a re-scrape: Facebook/Meta → Sharing Debugger (search “Facebook sharing debugger”), paste the URL, hit Scrape Again. LinkedIn → Post Inspector. X has no official tool. Its cache clears on its own, often within days, or you can share the URL with a throwaway query string to test the new card.
  3. 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.
  4. 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, and not necessarily yours. Keep exactly one set.
  5. 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, which is 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 one everyone gets, and catching a grey box in a draft costs thirty seconds instead of your launch.

08Pre-share checklist

That's one check. Your launch has about nine more.

A missing OG image is the most visible ShipReady finding, but it's rarely the only one. The same scan catches a default "Vite App" title, a missing favicon or viewport and console.logs in your production bundle. Once you verify the site is yours, it also checks for 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.

More launch-day guides