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:imageURL is unreachable or the file is too small - No card at all, just a bare link -> missing
twitter:cardmeta 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, orrobots.txtblockingTwitterbot - 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):
| Field | Rule |
|---|---|
| Format | PNG, JPEG, WebP (animated GIF accepted, but only the first frame is used) |
| Min dimensions | 300 x 157 px |
| Max dimensions | 4096 x 4096 px |
| Max file size | 5 MB |
| Aspect ratio | 1.91:1 (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 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):
- Append a cache-buster query string: tweet
https://yourdomain.com/article/foo?v=2instead of the bare URL. X caches by exact URL, so the new string is treated as a fresh page. - Wait 7 days — the cache expires on its own.
- 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.
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 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:
- Add
twitter:cardif missing -> fixes the largest share of “no card” cases - Make
twitter:image(orog:image) absolute https -> next biggest - Confirm the tags are in server-rendered HTML, not JS-injected -> the silent SPA killer
- Re-export image at 1200x630 PNG/JPEG under 1 MB -> fixes size/format failures
- Cache-bust with
?v=Nor wait 7 days -> stale-image cases - Whitelist
Twitterbotat the CDN/WAF, fixrobots.txt, move image topublic/-> the gated-image tail
How to confirm it is fixed
- 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' - Fetch the image as the crawler and confirm a clean
200:curl -A "Twitterbot/1.0" -I "https://yourdomain.com/og/your-image.png" - Paste the URL into opengraph.dev — it re-fetches fresh, so it reflects your current tags, not X’s cache.
- Final ground truth: post the URL (with
?v=2if 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:imageandtwitter:imageto 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" -Iagainst each new article’s OG image, assert200+ 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” 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.
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