DEV Community

Cover image for Open Graph Previews Breaking on Social Shares: A Debugging Scenario
Jay Barker
Jay Barker

Posted on

Open Graph Previews Breaking on Social Shares: A Debugging Scenario

Launch day. The blog post is live, the team is excited, and the marketing lead shares it everywhere at once. Then the messages start coming in.

On LinkedIn, the post is a bare URL with no card at all. In Slack, the preview shows the generic homepage image instead of the new post's artwork. And on Facebook, the card shows last week's draft title, which nobody wanted the world to see.

Three platforms, three different failures, one very public launch. If your Open Graph preview is not showing, or showing the wrong thing, the cause is almost never mysterious. Link previews are built by crawlers, not browsers, so the fix starts with seeing exactly what each crawler receives.

This post walks through that launch-day scenario step by step: how link previews work, how to see the page as a crawler does, what to check in your tags and your social card image, how to clear each platform's cache, and a small Node script that tests all of it before you hit share.

Disclosure: Jay Barker writes for SEO Site Checker, whose free tools are linked below. The debugging method and script work with any stack.

How Link Previews Actually Work

When someone pastes a URL into LinkedIn, Facebook, X, Slack or WhatsApp, the platform doesn't look at the page the way you do. Instead:

  1. The platform's crawler requests your URL, usually with its own user agent.
  2. It reads the HTML it receives, looking for og:* and twitter:* meta tags.
  3. It requests the image named in og:image or twitter:image.
  4. It builds the card and caches the result, often for days.

Three facts explain most broken previews. Crawlers usually don't run JavaScript. Each platform caches what it finds. And each has its own image rules.

Facebook's Open Graph guide for webmasters lists og:url, og:title, og:description and og:image as "the most basic meta tags that you should use for all content types," with og:type recommended. If you need a refresher on what each tag means, our earlier meta tags guide for developers covers definitions. This post is about what happens when they break.

The Scenario: Three Platforms, Three Failures

Here's what the team saw:

  • LinkedIn: no card at all, just the URL
  • Slack: a card, but with the homepage image and generic site title
  • Facebook: the right image, but last week's draft title

It's tempting to assume one bug. In practice, it's often three separate causes that happened to surface on the same day. The way to find them is to work from the outside in: what the crawler receives, then the tags, then the image, then the cache.

Step 1: See What the Crawler Sees

Start by requesting the page with each crawler's user agent and looking only at the meta tags:

# Facebook
curl -sL -A "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)" \
  https://example.com/launch-post | grep -iE 'og:|twitter:'

# LinkedIn (a commonly seen LinkedInBot user agent)
curl -sL -A "LinkedInBot/1.0 (compatible; Mozilla/5.0; +http://www.linkedin.com)" \
  https://example.com/launch-post | grep -iE 'og:|twitter:'

# Check status codes and redirects for any of them
curl -sIL -A "Twitterbot/1.0" https://example.com/launch-post
Enter fullscreen mode Exit fullscreen mode

The Facebook user agent string comes from Facebook's own documentation. The others are common examples, so check each platform's current documentation if you need an exact match for firewall rules.

Look for three things: whether the tags exist in the raw HTML at all, what status code each request returns, and whether you're being redirected somewhere unexpected, such as a login page or a bot challenge.

What the team found: the blog runs on a single-page React app, and the post's Open Graph tags were added in the browser after JavaScript loaded. The raw HTML that Slack's crawler received contained only the site-wide default tags. That explained Slack's homepage image.

Step 2: Check the Tags

Client-Rendered Tags

If tags are added by JavaScript in the browser, most social crawlers never see them. Single-page apps need the meta tags in the initial HTML response, through server-side rendering, static generation or prerendering. Most modern frameworks have a built-in way to set per-page metadata on the server. Use it, and confirm the result with curl, not your browser's inspector, which shows the page after JavaScript runs.

Relative or Wrong URLs

og:image should be an absolute HTTPS URL, such as https://example.com/images/launch-card.jpg, not /images/launch-card.jpg. Crawlers fetch the image separately, and a relative path may not resolve. Check that og:url matches the page's canonical URL too, or platforms may group shares under the wrong address.

Duplicates and Conflicts

A very common WordPress problem: an SEO plugin and the theme both output Open Graph tags, so the page has two og:title or og:image tags. Platforms may pick the first one they find, which might be the old or generic value. View the source and make sure each tag appears once.

Missing Width and Height

Facebook recommends specifying og:image:width and og:image:height "to ensure that the image loads properly the first time it's shared." Without them, the crawler may need to download and process the image before it can render the card, and the first share can appear without an image.

If you'd rather not hand-write tags, a free Open Graph generator builds the core og: tags from a simple form, ready to paste into your template.

Step 3: Check the Image Itself

Even with perfect tags, the card fails if the crawler can't get a usable image.

Access. The image must return a 200 status to bots. Common blockers include hotlink protection, a CDN or firewall bot challenge, signed or expiring image URLs, images on a password-protected staging server, and robots.txt rules that block the image path.

Format. Facebook's documentation lists image/jpeg, image/gif and image/png as accepted image types. X's summary_large_image requirements accept JPG, PNG, WEBP and GIF, and state that "SVG is not supported." So a WebP card image might work on X but fail on Facebook, and an SVG fails almost everywhere.

Size and shape. X's large image card supports "an aspect ratio of 2:1 with minimum dimensions of 300x157 or maximum of 4096x4096 pixels," and images "must be less than 5MB in size." Facebook's best practices recommend images "at least 1080 pixels in width for best display on high resolution devices." A safe default that works well across platforms is a 1200×630 JPG or PNG under about 1 MB. If your image is heavier, compress it before upload.

Content-Type. The server should send a Content-Type header that matches the file, such as image/jpeg for a JPG. A misconfigured server that sends text/html or application/octet-stream can make platforms reject a valid image.

What the team found: LinkedIn's crawler was hitting the CDN's bot protection when it requested the image. Instead of a JPG, it received an HTML "checking your browser" page with a 403 status, so LinkedIn had no image to show and dropped the card entirely. Adding an allow rule for known social crawlers fixed it.

Step 4: Clear Each Platform's Cache

Fixing the page doesn't fix shares that platforms have already cached.

  • Facebook: paste the URL into the Sharing Debugger. Facebook says it "also triggers a scrape of your page," and you can click to scrape again after fixing errors. Note that Facebook says "Images are cached based on the URL and won't be updated unless the URL changes," so if you replace an image file, give it a new URL or add a version query string such as launch-card.jpg?v=2.
  • LinkedIn: use LinkedIn's Post Inspector, which refetches the page and shows the preview LinkedIn will use.
  • X, Slack, Discord and WhatsApp: these refresh on their own schedules. If you need a fresh preview immediately, sharing a URL with a harmless query parameter, such as ?ref=launch, makes the platform treat it as a new link. Make sure your canonical tag still points to the clean URL.

What the team found: Facebook had scraped the post while it was still a draft with an old title. One click to scrape again in the Sharing Debugger fixed it.

A Script to Test Every Share Before Launch

Running curl by hand works, but it's easy to forget a platform. This Node script (Node 18 or later, no dependencies) fetches a page as Facebook, LinkedIn, X and Slack would, checks the core tags, then fetches the image as each crawler and checks status, type, file size and dimensions.

// og-check.mjs: fetch a page as social crawlers do and check its preview tags
// Usage: node og-check.mjs https://example.com/post   (Node 18+, no dependencies)
const CRAWLERS = {
  Facebook: 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)',
  LinkedIn: 'LinkedInBot/1.0 (compatible; Mozilla/5.0; +http://www.linkedin.com)',
  X: 'Twitterbot/1.0',
  Slack: 'Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)',
};
const REQUIRED = ['og:title', 'og:description', 'og:image', 'og:url', 'og:type'];

function readMeta(html) {
  const tags = {};
  for (const [tag] of html.matchAll(/<meta\b[^>]*>/gi)) {
    const key = /(?:property|name)=["']([^"']+)["']/i.exec(tag)?.[1]?.toLowerCase();
    const value = /content=["']([^"']*)["']/i.exec(tag)?.[1];
    if (key && value !== undefined) (tags[key] ||= []).push(value);
  }
  return tags;
}

function imageSize(buf) {
  if (buf[0] === 0x89 && buf[1] === 0x50) return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) }; // PNG
  if (buf[0] === 0xff && buf[1] === 0xd8) { // JPEG: walk segments to the SOF marker
    let i = 2;
    while (i < buf.length - 9) {
      const marker = buf[i + 1];
      if (marker >= 0xc0 && marker <= 0xc3) return { w: buf.readUInt16BE(i + 7), h: buf.readUInt16BE(i + 5) };
      i += 2 + buf.readUInt16BE(i + 2);
    }
  }
  return null;
}

async function checkImage(url, ua, problems) {
  const res = await fetch(url, { headers: { 'User-Agent': ua }, redirect: 'follow' });
  const type = res.headers.get('content-type') || 'unknown';
  if (!res.ok) { problems.push(`image returned HTTP ${res.status}`); return `HTTP ${res.status}`; }
  const buf = Buffer.from(await res.arrayBuffer());
  const kb = Math.round(buf.length / 1024);
  if (!type.startsWith('image/')) problems.push(`image Content-Type is ${type}, not an image`);
  if (type.includes('svg')) problems.push('SVG images are not supported for social cards');
  if (type.includes('webp')) problems.push('WebP works on X but not on Facebook (use JPG or PNG)');
  if (kb > 5120) problems.push(`image is ${kb} KB (X limit is 5 MB)`);
  const size = imageSize(buf);
  return `${type}, ${kb} KB${size ? `, ${size.w}x${size.h}` : ''}`;
}

const url = process.argv[2];
if (!url) { console.error('Usage: node og-check.mjs <url>'); process.exit(1); }

let failures = 0;
for (const [name, ua] of Object.entries(CRAWLERS)) {
  const problems = [];
  let summary = '';
  try {
    const res = await fetch(url, { headers: { 'User-Agent': ua }, redirect: 'follow' });
    if (!res.ok) problems.push(`page returned HTTP ${res.status}`);
    const tags = readMeta(await res.text());
    for (const key of REQUIRED) if (!tags[key]) problems.push(`missing ${key}`);
    for (const [key, values] of Object.entries(tags)) {
      if (key.startsWith('og:') && values.length > 1 && key !== 'og:image') problems.push(`duplicate ${key}`);
    }
    if (tags['og:image']?.length > 1) problems.push(`${tags['og:image'].length} og:image tags (first one usually wins)`);
    const image = tags['og:image']?.[0];
    if (image && !/^https:\/\//i.test(image)) problems.push(`og:image is not an absolute HTTPS URL: ${image}`);
    if (image && !tags['og:image:width']) problems.push('no og:image:width/height (first share may show no image)');
    if (image && /^https?:\/\//i.test(image)) summary = await checkImage(image, ua, problems);
  } catch (err) {
    problems.push(`request failed: ${err.message}`);
  }
  failures += problems.length ? 1 : 0;
  console.log(`\n${name}: ${problems.length ? 'PROBLEMS' : 'OK'}${summary ? `  (image: ${summary})` : ''}`);
  for (const p of problems) console.log(`  - ${p}`);
}
process.exitCode = failures ? 1 : 0;
Enter fullscreen mode Exit fullscreen mode

Run it against any public URL:

node og-check.mjs https://example.com/launch-post
Enter fullscreen mode Exit fullscreen mode

Here's the output from a test server set up to reproduce the scenario, with a CDN rule that blocks LinkedIn's crawler from the image:

Facebook: OK  (image: image/png, 4 KB, 1200x630)

LinkedIn: PROBLEMS  (image: HTTP 403)
  - image returned HTTP 403

X: OK  (image: image/png, 4 KB, 1200x630)

Slack: OK  (image: image/png, 4 KB, 1200x630)
Enter fullscreen mode Exit fullscreen mode

And against a single-page app page whose real tags are added by JavaScript, the crawlers only see the defaults:

Facebook: PROBLEMS
  - missing og:description
  - missing og:url
  - missing og:type
  - og:image is not an absolute HTTPS URL: /default.png
  - no og:image:width/height (first share may show no image)
Enter fullscreen mode Exit fullscreen mode

A clean page prints OK for every crawler with the image type, size and dimensions, and exits with code 0. Any problem exits with code 1, so you can add the script to a pre-launch checklist or a CI job that runs against your staging or production URLs.

The checks are deliberately simple. The script reads meta tags with regular expressions rather than a full HTML parser, and it reads dimensions only for PNG and JPEG files. Treat its output as a fast first pass, then confirm with each platform's own debugger before a big launch.

Prevention Checklist

  • Meta tags are in the initial HTML, not added by JavaScript
  • og:image is an absolute HTTPS URL to a 1200×630 JPG or PNG, under about 1 MB
  • og:image:width and og:image:height are set
  • Each Open Graph tag appears only once, with no plugin conflicts
  • Card images return 200 to social crawlers, with no bot challenges or expiring URLs
  • The server sends the correct Content-Type for images
  • The test script passes for every crawler before launch
  • The URL has been scraped in Facebook's Sharing Debugger and LinkedIn's Post Inspector before you announce
  • Image URLs get a new version whenever the image changes

For X, add twitter:card tags as well. A Twitter card generator creates them alongside your Open Graph tags. Afterwards, a website SEO checker gives you a broader view of the page's metadata and technical health.

FAQs

Why is my Open Graph image not showing?

The most common causes are tags added by JavaScript, so crawlers never see them, a relative or HTTP image URL, an image blocked by a firewall or hotlink protection, an unsupported format such as SVG, or a stale cached preview. Fetch the page and the image with a crawler's user agent to find out which.

Why does Facebook show an old image or title?

Facebook caches previews. Paste the URL into the Sharing Debugger to trigger a new scrape. If you replaced the image file but kept the same URL, Facebook may keep the old image, because it caches images by URL. Give the new image a different URL or add a version parameter.

How do I refresh a LinkedIn link preview?

Paste the URL into LinkedIn's Post Inspector, which refetches the page and shows the preview LinkedIn will use. If the preview is still wrong, check that LinkedIn's crawler can reach both the page and the image without being blocked or redirected.

What size should an Open Graph image be?

A 1200×630 pixel JPG or PNG under about 1 MB works well across most platforms. X's large image card uses a 2:1 ratio, accepts images from 300×157 up to 4096×4096 and requires files under 5 MB. Facebook recommends images at least 1080 pixels wide.

Do social media crawlers run JavaScript?

Generally, you shouldn't rely on it. Most link preview crawlers read the HTML your server returns, so tags added by client-side JavaScript are often missed. Render Open Graph tags on the server, through static generation or with prerendering, and verify them with curl.

Why does my link preview work on Facebook but not on X or WhatsApp?

Each platform has its own crawler, image rules and cache. X uses the twitter:card tag to decide which card to show, so add it alongside your Open Graph tags. Image formats, sizes and firewall rules can affect one crawler but not another. Test the page with each platform's user agent.

Wrapping Up

When an Open Graph preview is not showing, debug it in order. See what each crawler actually receives, check the tags, check the image, then clear each platform's cache. In our scenario, that meant server-rendering the tags for Slack, allowing social crawlers through the CDN for LinkedIn and rescraping the page on Facebook.

To get the tags right from the start, try the free Open Graph generator from SEO Site Checker, then run the script before every launch.

Top comments (0)