Twitter Card Image Not Showing When Sharing on X

Your X/Twitter link shows no image even though og:image is set. The 6 real causes, how to tell which one you have, and the fastest fix for each.

You paste your article URL into a tweet on X (formerly Twitter), expect the big preview card with a picture, and get either a bare link, a card with title-only, or a stale image you replaced weeks ago. Meanwhile the same URL renders cleanly on LinkedIn and Slack. This is almost never a “X is broken” problem.

Fastest fix: make sure the page’s server-rendered HTML has <meta name="twitter:card" content="summary_large_image"> plus an absolute https:// og:image, then confirm the crawler can fetch the image with curl -A "Twitterbot/1.0" -I <image-url> and expect 200. That clears the large majority of cases. If the tags are already correct and an old image still shows, it is X’s 7-day card cache — tweet the URL with a ?v=2 query string to force a re-crawl.

As of June 2026, X reads Open Graph tags as a fallback, so for most sites you do not need a separate twitter:image — but you do need at least twitter:card, and the tags must be in the server-rendered <head> because the X crawler does not run JavaScript.

Identify which case you are in (5 seconds)

  • Card renders with title + description but no image -> twitter:image / og:image URL is unreachable or the file is too small
  • No card at all, just a bare link -> missing twitter:card meta tag entirely, or the value is unrecognized, or the tags are injected by client-side JavaScript
  • Old image keeps showing after you replaced it -> X card cache (7 days)
  • Works on Facebook / LinkedIn but not X -> relative-path image, missing twitter:card, or robots.txt blocking Twitterbot
  • Works for new URLs, broken for one specific URL -> that URL’s image returns non-200 (signed URL expired, file deleted)
  • No card anywhere, page is a React/Vue/SPA app -> tags are added after page load and the crawler never sees them

Common causes, ranked by hit rate

1. Missing twitter:card declaration

Without <meta name="twitter:card">, X has no idea what shape of card to render. It will either fall back to a small summary card or render nothing at all. This is the single most common cause for “no card” on first share.

How to spot it: view-source on the page (not the DevTools Elements panel, which shows the post-JS DOM) and search for twitter:card. If it is absent from the raw HTML, this is your problem.

Fix: emit at minimum

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Article title" />
<meta name="twitter:description" content="One-sentence summary" />
<meta name="twitter:image" content="https://yourdomain.com/og/article.png" />

summary_large_image is the wide 1.91:1 card most publishers want. summary is the small square thumbnail card.

2. twitter:image is a relative path

X’s crawler does not resolve relative URLs. content="/og/foo.png" is sent to the crawler as the literal string /og/foo.png and the fetch fails.

<!-- Wrong: relative -->
<meta name="twitter:image" content="/og/article.png" />

<!-- Right: absolute https URL -->
<meta name="twitter:image" content="https://yourdomain.com/og/article.png" />

How to spot it: open view-source, inspect the content attribute. Anything starting with / or ./ is wrong.

3. Image violates X’s size / format rules

X has hard requirements for summary_large_image (current as of June 2026):

FieldRule
FormatPNG, JPEG, WebP (animated GIF accepted, but only the first frame is used)
Min dimensions300 x 157 px
Max dimensions4096 x 4096 px
Max file size5 MB
Aspect ratio1.91:1 (cropped if off-ratio)
ProtocolHTTPS only

A 200x200 favicon, a 6 MB hero shot, or an http:// URL all silently fail. The card may still render but with an empty image slot.

How to spot it:

curl -A "Twitterbot/1.0" -I "https://yourdomain.com/og/article.png"
# Expect: HTTP/2 200, content-type: image/png, content-length under 5000000
identify "https://yourdomain.com/og/article.png"   # ImageMagick - confirms dimensions

If content-length is over 5000000 or dimensions are under 300x157, re-export. Design at 1200x630 (1.91:1) so nothing gets cropped.

4. X cached an old crawl (7-day cache)

X caches card data per URL for up to 7 days after the first crawl. You fixed the meta tags and republished, but tweets still show the old card. There is no public “force re-scrape” button anymore — X killed cards-dev.twitter.com/validator back in 2022/2023 and never replaced it.

How to spot it: paste the URL into opengraph.dev or metatags.io; they fetch fresh and show what the current tags emit. If the validator shows the new image but X still shows the old one, it is the X cache.

Fix options (in order of effectiveness):

  1. Append a cache-buster query string: tweet https://yourdomain.com/article/foo?v=2 instead of the bare URL. X caches by exact URL, so the new string is treated as a fresh page.
  2. Wait 7 days — the cache expires on its own.
  3. Change the canonical URL (rare; only if the slug actually changed). X caches against the final destination of a redirect, so a new 301 target also re-crawls.

twitter:image points at a Cloudinary signed URL, a CDN that requires a Referer header, or a path behind your auth wall. The Twitterbot user-agent gets a 401 / 403 / 302-to-login.

curl -A "Twitterbot/1.0" -I "https://cdn.yourdomain.com/og/article.png"
# 200 + image/png means OK
# 401 / 403 / 302 means it is gated

Fix: serve OG images from your static public/ directory or whitelist the Twitterbot/1.0 user-agent at the CDN / WAF. Also check that robots.txt does not contain User-agent: Twitterbot followed by Disallow: / — that silently locks X out while LinkedIn and Slack (different bots) still work, which is exactly the “works everywhere but X” signature.

6. Meta tags injected by client-side JavaScript (SPA)

The X crawler does not execute JavaScript. If your og: / twitter: tags are added after load by React Helmet, Vue Meta, next/head on a client-only route, or any framework that hydrates on the client, the crawler fetches the initial HTML, finds no card tags, and renders a bare link. This is why DevTools (which shows the live DOM) looks correct but the card still fails.

How to spot it: run curl -s "https://yourdomain.com/article/foo" | grep -i 'twitter:card\|og:image'. If those tags are missing from the raw response but present in the browser Elements panel, they are JS-injected.

Fix: server-render or statically pre-render the meta tags (Next.js/Nuxt/Astro SSR or SSG, or framework metadata APIs), or serve a crawler-only meta-HTML via prerender/dynamic rendering. The tags must be present in the very first HTML response and inside <head>.

Open Graph as fallback (2026 behavior)

As of June 2026, X reads Open Graph tags when the twitter: namespace is missing. If you already emit og:image correctly, you can get away with just adding <meta name="twitter:card" content="summary_large_image" /> and skip twitter:image. This is the recommended minimum for content sites:

<meta property="og:title" content="Article title" />
<meta property="og:description" content="One-sentence summary" />
<meta property="og:image" content="https://yourdomain.com/og/article.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://yourdomain.com/articles/article-slug/" />
<meta property="og:type" content="article" />

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

Only override with twitter:image if you want a different image for X than for Facebook/LinkedIn.

Shortest fix path

In hit-rate order:

  1. Add twitter:card if missing -> fixes the largest share of “no card” cases
  2. Make twitter:image (or og:image) absolute https -> next biggest
  3. Confirm the tags are in server-rendered HTML, not JS-injected -> the silent SPA killer
  4. Re-export image at 1200x630 PNG/JPEG under 1 MB -> fixes size/format failures
  5. Cache-bust with ?v=N or wait 7 days -> stale-image cases
  6. Whitelist Twitterbot at the CDN/WAF, fix robots.txt, move image to public/ -> the gated-image tail

How to confirm it is fixed

  1. Fetch the page as the crawler does and check the tags are in the raw HTML:
    curl -s -A "Twitterbot/1.0" "https://yourdomain.com/articles/your-slug/" | grep -iE 'twitter:card|og:image'
  2. Fetch the image as the crawler and confirm a clean 200:
    curl -A "Twitterbot/1.0" -I "https://yourdomain.com/og/your-image.png"
  3. Paste the URL into opengraph.dev — it re-fetches fresh, so it reflects your current tags, not X’s cache.
  4. Final ground truth: post the URL (with ?v=2 if you are fighting a stale cache) from a throwaway X account and confirm the large image renders.

Prevention

  • Standardize on 1200x630 PNG or JPEG, target under 500 KB, served from your apex domain (not a signed CDN).
  • Use a helper that forces every og:image and twitter:image to be an absolute URL with protocol.
  • Make sure card tags are emitted server-side so the crawler sees them without running JavaScript.
  • Add a CI check: curl -A "Twitterbot/1.0" -I against each new article’s OG image, assert 200 + correct content-type + content-length under 5 MB.
  • Skip twitter:image unless you genuinely want a different image for X — let X use og:image as fallback to reduce per-template fields that can drift.
  • Auto-generate OG images from the article title (@vercel/og, satori, puppeteer) so “forgot to add an image” cannot happen.

FAQ

Q: X’s old card validator is dead. How do I test? A: Use opengraph.dev or metatags.io. Both fetch your URL fresh and show how X will render it. For absolute ground truth, post the URL from a throwaway X account and screenshot the result.

Q: I see the old image even after fixing meta tags. How long until X re-crawls? A: X’s card cache is about 7 days. There is no public force-refresh. The fastest workaround is a ?v=2 query string on the tweeted URL — X treats it as a new URL and re-crawls immediately.

Q: Do I need both og:image and twitter:image? A: No. As of June 2026 X reads og:image as a fallback. You only need both if you want different images per platform. The one tag X strictly requires is <meta name="twitter:card">.

Q: The tags look right in DevTools but the card still fails. Why? A: DevTools shows the live DOM after JavaScript runs; the X crawler does not run JavaScript. Check the raw HTML with curl -s <url> | grep twitter:card. If the tags are missing from curl but present in DevTools, they are JS-injected and must be moved to server-rendered HTML.

Q: My card has a picture but it is cropped weirdly. A: summary_large_image enforces 1.91:1. Source images that are 1:1 or 4:3 get center-cropped. Re-export at exactly 1200x630.

Q: Animated GIF — first frame only? A: Yes. X picks the first frame and freezes it. If the first frame is a black title card, the card looks blank. Use a still PNG/JPEG instead.

Q: My link unfurls fine in DMs but not in tweets. A: DMs use a separate inline preview that ignores cache more aggressively. Tweet rendering uses the card pipeline with the 7-day cache. Behavior diverging between the two confirms it is a cache issue, not a tag issue.

Tags: #SEO #Troubleshooting #Debug #Structured data #Twitter card #OG image