You share a link on X and the card has no picture — or a small square where a wide image should be. X does not tell you why. The card is built by X's crawler, Twitterbot, which downloads your page, reads its tags and downloads the image; anything that stops one of those three steps leaves the card empty. This page goes through the nine causes we see most, each reproduced on a small test site on 2026-11-08, with what it looks like in curl and in our X card validator, and how to fix it.
What we did and did not test. We served nine variants of one page (Node.js) and fetched each as Twitterbot with curl 8 from a second process, then ran each page's HTML through the parser our validator uses and each image's measured size and type through its X checks (the validator itself only fetches public sites, so the test pages could not be pasted into it). We did not post the test pages on X — they were not public — so what X itself displays comes from X's own documentation: its card troubleshooting guide (archived; the live page now redirects) and the card markup reference (archived too).
What we measured
| Cause | Page status for Twitterbot | What curl shows | What our validator reports |
|---|---|---|---|
1. No twitter:card |
200 | og:image present, no twitter:card |
warning: missing twitter:card |
2. Relative og:image |
200 | og:image=/img/ok.png |
error: relative image URL |
| 3. Tags added by JavaScript | 200 | tags only inside a <script> string |
error: no og:image (and no title) |
4. robots.txt disallows Twitterbot |
200 | page fetched fine — robots.txt is a rule the crawler follows, not a block | nothing on the page itself; check robots.txt |
| 5. Bot protection blocks Twitterbot | 403 | "Access denied", no tags | — our validator uses its own user agent, so it saw the page normally |
| 6. Image over 5 MB | 200, image 200 | image/png, 5,762,138 bytes |
error: file too large, 5.5 MB over X's 5 MB |
| 7. SVG image | 200, image 200 | image/svg+xml |
error: format SVG, X takes JPG, PNG, WebP, GIF |
| 8. Image URL returns HTML | 200, image 200 | text/html, 33 bytes |
the image is not an image |
| 9. Everything correct | 200, image 200 | image/png |
no issues |
The last row matters as much as the others: when every check passes and the card is still wrong, you are looking at X's cache (below).
The causes, one by one
1. No twitter:card — small square or no card
X's markup reference: "If an og:type, og:title and og:description exist in the markup but twitter:card is absent, then a summary card may be rendered" — the small square card, not the wide one. The image, title and description fall back to their og: tags, but the card type does not. Fix: add <meta name="twitter:card" content="summary_large_image"> to the page's <head>.
2. A relative image URL
<meta property="og:image" content="/img/card.png"> works in a browser and fails in crawlers, which do not resolve it against your domain. Use the full address: https://example.com/img/card.png. Our validator flags this as an error.
3. Tags added by JavaScript
Twitterbot reads the HTML your server sends and does not run scripts. In our test the tags were inserted by a script after load: a plain grep on the HTML still "found" them inside the script's text, while a real HTML parser — the one in our validator — found no og:image at all. That is how this cause hides: view source looks fine at a glance. Fix: render the tags on the server or at build time (every framework has a way; see our framework guides).
4. robots.txt disallows Twitterbot
User-agent: Twitterbot
Disallow: /blog/
A rule like this does not stop curl — our test page came back 200 — but X's troubleshooting guide names a robots.txt that blocks Twitter as a reason a card does not show. Check yours at https://example.com/robots.txt, and remember that a broad Disallow: / under User-agent: * covers Twitterbot too unless it has its own section.
5. Bot protection, firewall or .htaccess
The cause our validator cannot see from the outside: some firewalls and bot-protection modes answer crawlers differently from browsers. Our test server returned 403 to the Twitterbot/1.0 user agent and 200 to everyone else — including our validator, which identifies itself by its own name. X's guide also mentions Apache .htaccess deny rules against its IP ranges and CMS settings that block crawlers. Test with Twitterbot's user agent:
curl -s -A "Twitterbot/1.0" -o page.html -w "%{http_code}\n" https://example.com/post
A 403, a 503 or a page that says "checking your browser" means the crawler is turned away. Allow Twitterbot in the firewall's bot settings, or in the plugin that blocks it.
6. The image is too large
X's guide: "The web crawler will typically download up to 5MB images", and the markup reference says images "must be less than 5MB in size". Our 5.76 MB test image was flagged by the validator as over the limit. Social images rarely need more than a few hundred KB: 1200×630 as JPEG, or PNG for flat graphics, is plenty.
7. SVG (or another format X does not take)
X's markup reference: "JPG, PNG, WEBP and GIF formats are supported. Only the first frame of an animated GIF will be used. SVG is not supported." Our validator reported "format SVG" against X's JPG, PNG, WebP and GIF. Export a PNG or JPEG of the same design.
8. The image URL does not return an image
A missing file served as a 200 page, a login page, or a redirect to the home page: the image URL "works" but returns HTML. Ours returned text/html, 33 bytes. Check the image itself:
curl -s -A "Twitterbot/1.0" -o /dev/null -w "%{http_code} %{content_type} %{size_download}\n" https://example.com/img/card.png
You want 200, image/png or image/jpeg, and a size under 5 MB.
9. Everything is right — X's cache
X's guide: the crawler "re-indexes the meta information on your tag roughly every seven days". If the page was shared before you fixed it, X keeps showing the old card. There is no refresh button: share a URL X has not seen (add ?v=2), and give a changed image a new file name so no cache holds the old one under the same address.
A three-step check
- Paste the URL into our X card validator. It reads the tags with a real HTML parser and checks the image's size and format against X's limits: causes 1, 2, 3, 6, 7 and 8.
- Run the two curl commands above with
-A "Twitterbot/1.0". They catch causes 4 and 5, which depend on who is asking. - Paste the link into the X composer (do not post). If the validator and curl are clean but the draft shows an old card, it is the cache.
For every other platform — Facebook, LinkedIn, Slack, WhatsApp — the same causes apply with different crawlers and limits; our OG image debugging guide covers them, and the Twitter card guide lists every twitter: tag.
An image for every link
Once the card shows, it still needs a good picture for each page. Shareglint renders one from an HTML template and the page's title — as a JPEG or PNG well under X's limits, at an absolute HTTPS URL that bots can fetch — either through the API or with a signed URL in the page's og:image tag (signed image URLs). The free plan covers 100 images a month.