// debugging · May 10, 2026

Why your link preview isn't showing — and how to fix it

You shipped your site, shared the URL, and Slack rendered a sad empty card. Here are the ten causes we see most often, in frequency order, with the exact fix for each — plus a 7-step debug checklist for everything else.

The 12-second debug

Before you read the rest of this post, paste the broken URL into the Linkshot OG debugger. It fetches your URL the same way a social crawler would and tells you which Open Graph tags are present, valid, and reachable. Most "missing preview" mysteries collapse in one click.

The rest of this post is for the cases where the debugger tells you the tags look fine, but the preview still does not render.

1. The og:image URL is relative

How to spot it: view source on the broken page, look for og:image. If the content starts with / and not https://, this is your problem.

html
<!-- BROKEN: relative URL -->
<meta property="og:image" content="/og.png" />

<!-- FIXED: absolute URL -->
<meta property="og:image" content="https://example.com/og.png" />

Crawlers do not know what your origin is. A relative URL silently fails. This is the single most common cause we see — often introduced when an SSR framework auto-generates the meta tag from a relative path and nobody has set a base URL.

2. The image lives behind auth

The OG URL points at /api/og, which redirects unauthenticated requests to /login. Crawlers are unauthenticated; they get the login page; the preview is empty.

Fix: serve OG images from a public path that never redirects. If you generate them dynamically, your generator endpoint must return a 200 with a valid image content-type for any User-Agent — including ones with no cookies.

bash
# Test as a crawler — no cookies, no Accept header
curl -I -A "facebookexternalhit/1.1" https://example.com/og.png
# Look for: HTTP/2 200 and Content-Type: image/png

3. The platform cache is stale

You shipped a new image but X or Facebook still shows the old one. They cache OG images for days to weeks.

  • Facebook: Sharing Debugger → "Scrape Again."
  • LinkedIn: Post Inspector → "Refresh."
  • X / Twitter: no exposed refresh in 2026. Append a query string (?v=2) to the page URL or the og:image URL.
  • Slack, iMessage, Discord: the cache is short-lived (hours, not weeks). Wait or change the URL.

4. Wrong dimensions

Facebook silently rejects images smaller than 200×200. X renders only a small square card unless the image meets the 1200×630 (or 2:1) ratio for summary_large_image. Going too large hurts too — files over 5MB get truncated.

Safe defaults: 1200×630 PNG, under 1MB. This renders correctly on every platform we know of.

5. Missing twitter:card meta

X falls back to a tiny square preview unless you opt into the wide card explicitly:

html
<meta name="twitter:card" content="summary_large_image" />

That single line is the difference between a thumbnail and a full-bleed image card on X.

6. Bot user-agent blocked at the firewall

Cloudflare, AWS WAF, and many bot-management services block unfamiliar user-agents by default. Crawler bots do not identify as a normal browser. If your firewall blocks them, you blocked your own previews.

The user-agents to allow:

text
facebookexternalhit/1.1
Twitterbot/1.0
Slackbot-LinkExpanding 1.0
LinkedInBot/1.0
WhatsApp/2
Discordbot/2.0
TelegramBot
Applebot/0.1
Googlebot/2.1

On Cloudflare, this is in Security → Bots → Configure Super Bot Fight Mode. Add an allow rule for any URL that matches known social-crawler user agents.

7. Wrong content-type header

Your OG image URL returns 200 OK but with Content-Type: text/html. This happens when:

  • Your CDN serves an HTML error page when the origin times out.
  • Your dynamic OG generator throws and returns the framework error page instead of failing properly.
  • You hit a CORS preflight rule that returns 200 with no body.

Test the actual content type:

bash
curl -I https://example.com/og.png
# Must include: Content-Type: image/png  (or image/jpeg)

8. og:url disagrees with the canonical

Some platforms (Facebook in particular) cache OG data keyed by the URL in og:url, not the URL the user pasted. If og:url points at a different page (typo, leftover from a redesign, hardcoded to production while testing in staging), the preview reflects the other page.

Rule: the URL in og:url must match the page's canonical and the URL the user actually shares. No trailing slash mismatches, no http/https mismatches.

9. Mixed content (http image on https page)

If your page is on https:// but the og:image is http://, modern crawlers (and most modern browsers) refuse to render it. The fix is trivial: always serve OG images over HTTPS, even if the rest of your site is somehow not.

10. The OG card drifted from the live design

Not technically "broken" but functionally so: the preview shows an outdated logo, an old headline, a deprecated product screenshot. Visitors think the link is wrong and don't click.

This is the failure mode template-based OG generation (Vercel OG, Satori, etc.) creates by design — the OG template is a separate codebase from your real page. Updating the page does not update the card. Months pass; the card silently decays.

The structural fix is to make your OG image be your page — a screenshot of the live URL. Linkshot does this. See dynamic OG images in Next.js for the integration code, or the comparison vs Vercel OG for trade-offs.

The 7-command unblock checklist

Run these in order. The first one that fails is your bug.

bash
# 1. Page reachable as a crawler?
curl -I -A "facebookexternalhit/1.1" https://example.com/the-page

# 2. og:image meta present and absolute?
curl -s -A "facebookexternalhit/1.1" https://example.com/the-page \
  | grep -i 'og:image'

# 3. og:image URL reachable?
OG_URL=$(curl -s https://example.com/the-page | grep -oP 'og:image[^>]+content="\K[^"]+')
echo "$OG_URL"
curl -I -A "facebookexternalhit/1.1" "$OG_URL"

# 4. og:image content-type is image/*?
curl -sI -A "facebookexternalhit/1.1" "$OG_URL" | grep -i content-type

# 5. og:image dimensions (≥600×315)?
curl -s -A "facebookexternalhit/1.1" "$OG_URL" | file -

# 6. og:url matches the canonical?
curl -s https://example.com/the-page \
  | grep -E 'og:url|rel="canonical"'

# 7. twitter:card present for X previews?
curl -s https://example.com/the-page | grep 'twitter:card'

If all seven pass and the preview is still broken, the issue is platform-side caching — use the platform debugger to scrape again, or version the og:image URL.

When to stop debugging and switch approach

If you find yourself debugging OG images more than once a quarter, the underlying architecture is wrong. Manually generated static cards rot. JSX templates drift from the live design. The fixes pile up.

The simplest long-term answer is to make the OG image be the page — a real Chromium screenshot of the live URL, served from a CDN. There is nothing to maintain because there is no second copy of the design. Linkshot does exactly this; for the screenshot-to-OG workflow, the integration is one meta tag.

Frequently asked questions

My link preview works in iMessage but not in Slack — why?

iMessage and Slack use different crawlers (Apple's Linkbot and Slackbot). Slackbot is stricter about absolute URLs, redirects, and content-type headers. Run the URL through Slack's preview by pasting it into a private DM with yourself; if it fails there, it almost always means the og:image URL is relative or returns a non-image content type.

I updated my OG image but Facebook still shows the old one.

Facebook caches OG images for ~30 days. Open the Facebook Sharing Debugger, paste your URL, and click "Scrape Again." That forces a recrawl. If you change the og:image content URL itself (e.g. add ?v=2), the cache miss is automatic and the new image appears immediately.

Why does my preview show only the title, not the image?

Almost always one of three things: the og:image meta is missing, the URL is relative (not absolute), or the image is behind authentication. Run the page through a debugger that fetches it the same way a crawler would — most browsers will show the image when you visit it directly because you have a session cookie that the crawler does not.

My OG image is 1200×630 PNG but Twitter still shows a small thumbnail.

Twitter requires `<meta name='twitter:card' content='summary_large_image' />` to render the wide preview. Without it, Twitter falls back to the small `summary` card regardless of the image dimensions.

How do I clear an OG cache without changing the URL?

Each platform has its own debugger that re-fetches: Facebook (Sharing Debugger → Scrape Again), LinkedIn (Post Inspector → Refresh), X (Card Validator). Slack and iMessage have no exposed re-fetch — you wait for the natural TTL or ship a new URL.

Crawlers can't reach my OG image but my browser can. What is happening?

Two common causes. First: a Cloudflare bot challenge or rate limit returning HTML/403 to crawler user-agents. Allow user-agents like facebookexternalhit, Twitterbot, Slackbot, LinkedInBot in your firewall. Second: a CDN serving the image from a private origin that requires a signed cookie — the crawler has no cookie.

Does the og:image URL need to be on the same domain as the page?

No. Most platforms accept cross-origin OG images as long as the URL is absolute, returns a 200, and serves a valid image content-type. CDNs (e.g. Cloudflare R2, S3, Linkshot) are routinely used to host OG images while the page lives on the main domain.

How do I debug an OG image that returns a 200 but renders blank?

Open the URL directly in a browser. If you see a blank page, the issue is server-side — wrong content-type, corrupted PNG, or the response is HTML rather than an image. If the image renders fine in the browser but blank in Slack/X, the issue is dimensions (under 200×200), file size (over 5MB), or a transparent PNG that the platform refuses to composite.

Stop debugging OG images

Linkshot screenshots your live page and serves it from the edge. No relative URLs, no auth walls, no CSS subset, no drift. Add one <meta> tag and move on.

7-day free trial · no credit card required