every check, explained
this is the exact list card doctor runs. none of it is theory — each entry broke a real card in production at least once. grouped the way you should fix them: meta first (free), then the image, then the transport, then the stuff you cannot rush.
the meta
twitter:card present and valid
why it breaks cards: without <meta name="twitter:card" content="summary_large_image"> x falls back to whatever it can guess, and guesses small. fix: set the tag explicitly. summary_large_image for the full-bleed image, summary for a thumbnail.
title & description exist
why: an empty title or description renders an empty-looking card. fix: set og:title / og:description (or twitter: variants). x truncates descriptions around 200 characters — front-load the point.
a card image exists (twitter:image / og:image)
why: no image tag, no image — the single most common cause. fix: point it at an absolute https url. x reads twitter:image first, falls back to og:image.
canonical url signals (rel=canonical, og:url)
why: if you share cache-busting variants (?s=2) to escape x's poisoned cache, a canonical/og:url can collapse every variant back onto the root's cached card. fix: keep rel=canonical for google if you like, but drop og:url when you rely on variants.
the image
image is fetchable (2xx)
why: a 403/404/timeout here is an instant no-image card. fix: the url must be publicly fetchable — no auth, no hotlink protection, no waf rules blocking datacenter crawlers.
image content-type is actually an image
why: a file served as application/octet-stream (a missing entry in your server's mime map — we shipped exactly this bug) gets rejected even when the bytes are a perfect jpeg. fix: serve image/jpeg or image/png.
image framing: content-length, not chunked
why: browsers tolerate chunked responses; some image pipelines are pickier. fix: set an explicit Content-Length on static files. one line in most servers.
image size & dimensions
why: over 5mb is rejected; under 600×315 renders tiny or not at all. fix: 1200×630 (1.91:1), well under 5mb.
image format detail
why: progressive jpegs and interlaced pngs trip older decoders in some pipelines. fix: export baseline jpeg / non-interlaced png.
no query string in the image url
why: x's image fetcher is historically flaky with og.png?v=3-style urls — we lost days to this. fix: version in the filename instead: og-v3.jpg.
the transport
tls certificate verifies
why: an incomplete chain works in browsers (they fetch intermediates) but fails for many crawlers. fix: serve the full chain; test from a clean client, not your browser.
robots.txt doesn't block the bot
why: a Disallow: / (or a twitterbot-specific rule) kills the crawl at the door. fix: allow it explicitly. a missing robots.txt (404) is fine.
redirect chain is short
why: long chains (http→https→www→apex→page) burn the crawler's patience. fix: one hop max to the final url.
html isn't no-cache
why: some card caches re-fetch no-cache pages on every render attempt instead of storing the metadata once. fix: a short public max-age on html is fine.
response times
why: crawlers give up on slow origins, especially for the image. fix: page and image should each answer in a couple of seconds from a us vantage.
the stuff you cannot rush
domain age (rdap)
why: x withholds card images for very young domains even when every byte is correct — verified in production with a domain that passed all other checks. fix: age and real shares heal it; or bypass the image leg entirely by hosting the cover on pbs.twimg.com (upload once in any tweet, copy the image address) — x always renders its own cdn.
the card cache
why: x locks a url's card at first crawl and the old validator is dead. fix: you cannot purge; test and share on fresh query-parameter urls.