DEV Community

Cover image for Why your Open Graph image doesn't show up when you share a link
Accreditly
Accreditly

Posted on • Originally published at html2img.com

Why your Open Graph image doesn't show up when you share a link

You paste a link into Slack and the preview comes back as a bare grey rectangle. Or the title shows and the image does not. Or LinkedIn serves artwork from three deploys ago. You open devtools, the tags are right there, and the card is still wrong.

The reason is nearly always the same one. The thing reading your page is not a browser. A social crawler makes one anonymous request, parses the HTML that came back over the wire, fetches the image, and leaves. No session, no JavaScript engine, no patience. We have published the full version of this with the platform specifics on the HTML to Image blog, in Why your Open Graph image doesn't show up when you share a link, and this post walks through the eight causes and how to tell them apart.

What happens when someone shares your URL

Four steps, and each fails independently:

  1. The platform requests your URL with a crawler user agent, following redirects.
  2. It parses the served HTML for og: and twitter: tags.
  3. It resolves og:image to an absolute URL and fetches it, often reading only enough of the file to get its dimensions.
  4. It builds a card using its own rules about which tag wins, how to crop, and how much of the title to keep.

Working out which step broke is most of the job.

1. The tags are in devtools but not in the response

The most common cause by a distance. If your head is set client-side, a Vue app assigning meta on mount, a React app using a client-only helmet component, a CMS script patching the head after load, the crawler sees none of it. Devtools shows the DOM after your JavaScript has run. The crawler reads the document as served.

Side by side comparison of a page head in devtools with three Open Graph tags present, and the same page as served to a crawler with no Open Graph tags at all

One command tells you:

curl -sL https://example.com/pricing | grep -i 'og:\|twitter:'
Enter fullscreen mode Exit fullscreen mode

Empty output means the tags have to move into server-rendered HTML. In Next.js that is the metadata export or generateMetadata in a server component, not anything set in useEffect. In Nuxt it is useSeoMeta on a server-rendered route. In a static site generator it is the head partial at build time.

2. og:image is a relative path

<meta property="og:image" content="/images/og/pricing.png">
Enter fullscreen mode Exit fullscreen mode

Valid HTML, useless to a crawler. The protocol asks for an absolute URL including the scheme, and several platforms drop the image rather than guess your host.

<meta property="og:image" content="https://example.com/images/og/pricing.png">
Enter fullscreen mode Exit fullscreen mode

Same problem with protocol-relative // URLs, and with anything still pointing at localhost because the base URL comes from an environment variable nobody set in production.

3. The image is not reachable by an anonymous bot

The image fetch is as anonymous as the page fetch. Hotlink protection that wants a same-origin referer, Cloudflare Bot Fight Mode on the asset path, a robots.txt rule over the image directory, an expired signed CDN URL, or a private bucket object will all kill it.

curl -sI https://example.com/images/og/pricing.png | head -n 3
Enter fullscreen mode Exit fullscreen mode

You want a 200 and an image content type. A 403 for curl is a 403 for Facebook.

4. No og:image:width and og:image:height

Seeing your URL for the first time, a platform has no idea how big the image is, so it downloads and measures it before laying out the card. Several will render the card without the image rather than wait, then get it right on the second or third share. Your launch post has already been seen by then.

<meta property="og:image" content="https://example.com/images/og/pricing.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:type" content="image/png">
<meta property="og:image:alt" content="Pricing plans for Northgate">
Enter fullscreen mode Exit fullscreen mode

5. The image is the wrong size, ratio or format

1200 by 630 renders cleanly everywhere. Below 600 by 315, Facebook and LinkedIn downgrade to a small square thumbnail beside the text. Over 8MB, the image is dropped. SVG is not read as an og:image anywhere that matters, and WebP support is patchy enough that PNG or JPEG remains the safe answer.

Then the crop. Everything centre-crops toward roughly 1.91:1 and square unfurls keep only the middle band, so a logo or headline near an edge is a logo or headline you sometimes lose.

6. twitter:card is missing so X renders the small card

X falls back to og: tags for the title, description and image, which is why people skip the twitter: namespace. The one tag you cannot skip is the card type. Without it you get summary, a small square thumbnail beside the text, instead of the full-width image card.

<meta name="twitter:card" content="summary_large_image">
Enter fullscreen mode Exit fullscreen mode

7. The URL you tested is not the URL people share

Tags are read from the final URL after redirects, and what ends up in the wild is rarely the clean URL you tested. Campaign parameters, a trailing slash difference, a locale prefix, an AMP variant or an http to https hop can each resolve to a document with different tags. Test the canonical URL, then test the exact string your campaign is about to use.

8. Facebook is still showing a card you fixed last week

Meta caches the first scrape hard and can hold it for days. Deploying a fix does not invalidate it, so run the URL through Meta's Sharing Debugger once and use Scrape Again. This one matters mostly because it makes the other seven harder to diagnose. If a live fetch shows correct tags and Facebook shows the old card, the cache is stale rather than your fix being wrong.

Checking the lot in one pass

The official debuggers have thinned out. Meta's needs a Facebook login and only covers Facebook. X removed the rendered preview from its Card Validator, so it now tells you a card is valid without showing you the card. Neither says anything about Slack, Discord or WhatsApp.

We built the Open Graph Checker to run the whole sequence in one request: fetch as a crawler without executing JavaScript, follow redirects and read tags from the final URL, resolve relative image URLs, then read the image's real dimensions, type and file size from the file rather than from what the tags claim. It renders the card as Facebook, X, LinkedIn, Slack, Discord and WhatsApp would each build it, scores the page, and prints a corrected tag block. No login, and it reads live rather than from a cache.

The failures it separates fastest are the ones that look identical from outside: tags missing because they are client-side, an image URL that resolves but returns a 403, an image that is present but 400 pixels wide, and a page that redirects somewhere with different tags.

The block you are aiming for

<meta property="og:type" content="article">
<meta property="og:url" content="https://example.com/pricing">
<meta property="og:site_name" content="Northgate">
<meta property="og:title" content="Pricing that scales with your traffic">
<meta property="og:description" content="Per-render pricing with no seat fees, and a free tier that covers a small site.">
<meta property="og:image" content="https://example.com/images/og/pricing.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:type" content="image/png">
<meta property="og:image:alt" content="Pricing plans for Northgate">

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Pricing that scales with your traffic">
<meta name="twitter:description" content="Per-render pricing with no seat fees, and a free tier that covers a small site.">
<meta name="twitter:image" content="https://example.com/images/og/pricing.png">
Enter fullscreen mode Exit fullscreen mode

Titles under about 60 characters and descriptions under about 155, or they truncate in most placements.

Stopping it happening again

Share metadata rots quietly. It breaks during framework migrations, CMS moves and route refactors, and nothing in your test suite notices because the page still returns a 200. A crude assertion over your important URLs catches most of it:

for url in \
  https://example.com/ \
  https://example.com/pricing \
  https://example.com/docs
do
  html=$(curl -sL "$url")
  echo "$html" | grep -q 'property="og:image"' \
    && echo "ok   $url" \
    || echo "FAIL $url"
done
Enter fullscreen mode Exit fullscreen mode

Run it in CI after deploy, and put your top twenty URLs through a checker after any migration, while the traffic that would have hit those broken cards is still recoverable.

The longer version, including how to generate the card image itself when the answer turns out to be that you never had one, is on the HTML to Image blog.

Which of the eight has bitten you? Mine is client-side tags, twice on the same project.

Top comments (0)