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 “Twitter is broken” problem — it is one of four very specific things: missing twitter:card declaration, an og:image / twitter:image URL the crawler cannot fetch, an image that violates X’s size/format rules, or X’s 7-day card cache holding an old crawl. In 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.
Identify which case you are in (5 seconds)
- Card renders with title + description but no image →
twitter:image/og:imageURL is unreachable or the file is too small - No card at all, just a bare link → missing
twitter:cardmeta tag entirely, ortwitter:cardvalue is unrecognized - Old image keeps showing after you replaced it → X card cache
- Works on Facebook / LinkedIn but not X →
twitter:imageis a relative path that the X crawler does not resolve, ortwitter:cardis missing - Works for new URLs, broken for one specific URL → that URL’s image returns non-200 (signed URL expired, file deleted)
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 and search for twitter:card. If absent, 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:
| Field | Rule |
|---|---|
| Format | PNG, JPEG, WebP (animated GIF accepted but only first frame used) |
| Min dimensions | 300 x 157 px |
| Max dimensions | 4096 x 4096 px |
| Max file size | 5 MB |
| Aspect ratio | 1.91:1 (will be cropped if off-ratio) |
| Protocol | HTTPS only |
A 200x200 favicon, a 6 MB hero shot, or an http:// URL all silently fail. The card may still render but without the 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 5,000,000 or dimensions are under 300x157, re-export.
4. X cached an old crawl (7-day cache)
X caches card data per URL for up to 7 days. You fixed the meta tags and republished but tweets still show the old card. There is no public “force re-scrape” button anymore — Twitter killed cards-dev.twitter.com/validator in 2023.
How to spot it: paste the URL into opengraph.dev or cardvalidator.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):
- Append a cache-buster query string: tweet
https://yourdomain.com/article/foo?v=2instead of the bare URL. - Wait 7 days — the cache expires.
- Change the canonical URL (rare; only if the slug actually changed).
5. Image behind auth, CDN signing, or hotlink protection
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's gated
Fix: serve OG images from your static public/ directory or whitelist the Twitterbot/1.0 user-agent at the CDN.
Open Graph as fallback (2026 behavior)
As of 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 in 2026:
<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:
- Add
twitter:cardif missing → fixes ~45% of “no card” cases - Make
twitter:image(orog:image) absolute https → fixes ~25% - Re-export image at 1200x630 PNG/JPEG under 1 MB → fixes ~15%
- Cache-bust with
?v=Nor wait 7 days → fixes ~10% - Whitelist Twitterbot at the CDN / move image to public/ → the worst 5%
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:imageandtwitter:imageto be an absolute URL with protocol. - Add a CI check:
curl -A "Twitterbot/1.0" -Iagainst each new article’s OG image, assert 200 + correct content-type + content-length under 5 MB. - Skip
twitter:imageunless you genuinely want a different image for X — let X useog:imageas 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” can’t happen.
FAQ
Q: Twitter’s old card validator is dead. How do I test? A: Use opengraph.dev or cardvalidator.io. Both fetch your URL fresh and show how X will render it. For the 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 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.
Q: Do I need both og:image and twitter:image?
A: No. As of 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: 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 will be 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.
Related articles
- OG image not showing when shared
- OG image not appearing on social shares
- Structured data rich results warning
- Website JSON-LD inconsistency
Tags: #SEO #Troubleshooting #Debug #Structured data #Twitter card #OG image