Your Twitter card is not showing an image on X, even though your meta tags are perfect? It might not be your meta tags at all. Mine were fine and my cards were still dead for weeks.
Every time I shared XPMetric.com on X, I got a bare text link. No card, no image, nothing. Meanwhile my other project ProofSwap.me showed a beautiful preview card every single time.
Same developer. Same meta tags. Same setup, basically. One worked, one didn’t.
I want to walk you through the whole thing, because if your cards are dead and you already “fixed” everything, this might be the reason. And you will never find it in your server logs. That’s the evil part.
The stuff I tried first (that you probably tried too)
If you google “Twitter card not showing image” you get the standard checklist. I did all of it:
- Checked
twitter:card,twitter:image, andog:imagetags. All correct. - Made sure the image is under 5MB and publicly reachable. Yep.
- Checked
robots.txtisn’t blocking Twitterbot. It wasn’t. - Stripped weird
Varyheaders that Next.js adds (X’s crawler hates those, by the way). Done. - Added
Content-Lengthto the OG images. Done. - Curled my site as Twitterbot to verify. Perfect response, every time.
And here is the trap. That last check lies to you.
When you run curl -A "Twitterbot/1.0" on your own site, you test YOUR request. Not the crawler’s request. My curl went to https, got a 200 and a perfect card. The real crawler never did that. Not once.
How I actually found it
Server logs showed zero Twitterbot visits. Weeks of nothing. So either X never crawled me, or something ate the requests before they reached my server.
My site sits behind Cloudflare. And Cloudflare’s analytics can show you things your origin server will never see. I queried the GraphQL analytics API filtered by user agent, and there it was:
X’s crawler hit my domain 20 to 50 times per day. Every single request was answered with a 301 redirect straight from Cloudflare’s edge. The field originResponseStatus said 0. Meaning my server was never contacted. That’s why my logs were empty. The requests died at the CDN.
Four straight days of data. 51 crawls on one day alone. All 301. Zero successful fetches. (If you want to watch your own server this closely, my post on Linux monitoring tools is a good starting point, though in this case the CDN was the one holding the real logs.)
The root cause: two dumb things stacked on top of each other
Thing 1: X stores bare domains as http, not https
When you type a bare domain on X, like just “xpmetric.com” in a tweet or your bio, X stores the link as http://xpmetric.com. Not https. Hover over any bare domain link in a tweet and look at the tooltip. It says http. That’s an ancient web convention and X never changed it.
Thing 2: X’s card crawler does not follow redirects
At all. It fetches the stored URL exactly once, and whatever comes back is what you get. A redirect means no card. This is documented nowhere prominent and it’s been like this for years.
Now add Cloudflare’s Always Use HTTPS setting. Sounds harmless, right? Every http request gets a 301 to https, at the edge. Great for humans.
But for the crawler the chain looks like this. X crawls http://yourdomain.com, Cloudflare answers 301, the crawler gives up, and X caches “this domain has no card”. Forever. Every share shows a bare link. And because the redirect happens at the edge, your origin logs show absolutely nothing. You are debugging a ghost.
Why did ProofSwap work then?
Its card entry got established through https shares early on, so X refreshes it over https where everything is fine. XPMetric got stuck on the http path and could never get out. Chicken and egg. (ProofSwap is my own social proof tool for indie makers, in case you’re wondering why one random project got lucky.)
The fix: give the crawler a 200 on http
You need to serve X’s crawler a 200 on http instead of a redirect. Sounds scary. It isn’t. Three steps.
1. Turn OFF “Always Use HTTPS” in Cloudflare (SSL/TLS, Edge Certificates).
2. Replace it with a Redirect Rule so humans still get redirected exactly like before, while the social crawlers are left alone:
Expression:
(not ssl and not (http.user_agent contains "Twitterbot"
or http.user_agent contains "facebookexternalhit"
or http.user_agent contains "LinkedInBot"
or http.user_agent contains "Slackbot"
or http.user_agent contains "Discordbot"
or http.user_agent contains "TelegramBot"
or http.user_agent contains "WhatsApp"))
Then:
301 to concat("https://", http.host, http.request.uri.path)
Preserve query string: on
3. On your origin, add an http block that serves those crawler user agents the real page and redirects everyone else. I use Caddy, so mine looks like this:
http://xpmetric.com {
@social_crawler header_regexp ua User-Agent (?i)(twitterbot|facebookexternalhit|linkedinbot|slackbot|discordbot|telegrambot|whatsapp)
handle @social_crawler {
reverse_proxy app:3000
}
handle {
redir https://xpmetric.com{uri} permanent
}
}
The same idea works in nginx or anything else. Match the crawler user agents, serve them the page, redirect everyone else.
What about security?
Fair question, I had it too. Humans get the identical 301 as before. If you send HSTS headers (you should), real browsers never even attempt http. The only thing served over http is your public landing page, to a handful of bot user agents. Nothing that https doesn’t already serve to anonymous visitors anyway.
Verify it works
Two quick curls tell you if the fix landed:
# Should return 200 with your meta tags:
curl --http1.1 -A "Twitterbot/1.0" http://yourdomain.com/
# Same curl without the user agent should return 301:
curl --http1.1 http://yourdomain.com/
Within the first hour after deploying, the real crawler fetched my page successfully 5 times. First successful crawls in weeks.
One thing to know: X caches card results hard. After the fix it takes hours, sometimes days, until bare links show cards again. A URL with a fresh query param like ?v=2 forces a new cache entry if you want to test faster. The X Card Validator can also nudge a re-crawl.
The checklist if your Twitter card is not showing an image
- Hover your shared link on X. Tooltip says http? You might have this exact problem.
- Check your origin logs for Twitterbot. Empty? Something upstream is eating the requests.
- Behind Cloudflare? Check Security Events and the GraphQL analytics for the crawler’s real requests and what status they got.
- Look for edge 301s with origin status
0. That’s the smoking gun. - Stop trusting your own curls. Test http, not just https.
Frequently asked questions
Why is my Twitter card not showing an image even though my meta tags are correct?
Because valid meta tags only matter if X’s crawler actually reaches them. If something between X and your server returns a redirect (like Cloudflare’s Always Use HTTPS on an http request), the crawler never sees your tags. It fetches once, gets a 301, and gives up.
Does the X (Twitter) card crawler follow redirects?
No. Twitterbot fetches the stored URL exactly once and uses whatever comes back. If that response is a 301 or 302, you get no card. This is the single most overlooked cause of dead cards on self-hosted sites.
Why does X crawl my site over http instead of https?
When you paste a bare domain into a tweet or your bio, X stores it as http:// by default. It’s an old convention X never updated. Combined with an http-to-https redirect at your CDN, that http path becomes a dead end for the crawler.
Is it safe to serve crawlers over http?
Yes, within reason. Real browsers still get redirected to https, and HSTS stops them from ever trying http. You only serve your public landing page over http to a short list of bot user agents, which is content https already serves to anonymous visitors.
How long until my card shows up after the fix?
X caches card results aggressively, so expect hours to a couple of days for bare links. Add a fresh query parameter like ?v=2 or run the URL through the X Card Validator to force a new fetch and test faster.
Final thoughts
The wildest part is that every individual piece here is reasonable. X defaulting bare domains to http, a crawler that doesn’t follow redirects, a CDN setting that redirects http. Each one is fine on its own. Together they killed my preview cards for weeks and left zero traces in my logs.
Hope this saves someone the debugging marathon. If your Twitter card is not showing an image and your meta tags are fine, check the http path. That’s probably it.