你把文章 URL 粘进 X(原 Twitter)的推文输入框,预期看到带大图的卡片,结果要么是干巴巴的链接,要么只有标题没图,要么显示的还是两周前已经换掉的旧图。同样的 URL 在 LinkedIn、Slack 都好好的。这几乎从来不是「X 又坏了」。
最快的修法:确认页面服务端渲染出来的 HTML 里有 <meta name="twitter:card" content="summary_large_image">,加上一个绝对的 https:// og:image,然后用 curl -A "Twitterbot/1.0" -I <图片URL> 确认爬虫能抓到、返回 200。这能解决绝大多数情况。如果 meta 已经是对的、却还在显示旧图,那就是 X 的 7 天卡片缓存——推这条 URL 时在末尾加 ?v=2 强制重抓即可。
截至 2026 年 6 月,X 会把 Open Graph 当 fallback 来读,所以对大多数站点你不需要单独的 twitter:image——但你至少要有 twitter:card,而且这些标签必须出现在服务端渲染的 <head> 里,因为 X 爬虫不执行 JavaScript。
5 秒判断你是哪种情况
- 卡片有标题和描述但没图 ->
twitter:image/og:image抓不到或图太小 - 完全没有卡片,只是裸链接 -> 整个
twitter:cardmeta 缺失,或content值不被识别,或标签是被客户端 JavaScript 注入的 - 换了图后还是显示旧图 -> X 卡片缓存(7 天)
- Facebook / LinkedIn 都正常,只有 X 不行 -> 图用了相对路径、
twitter:card缺失,或robots.txt挡了Twitterbot - 新 URL 都正常,只有某一条不行 -> 这条 URL 的图返回非 200(签名 URL 过期、文件已删)
- 整站都没卡片,页面是 React/Vue/SPA -> 标签是页面加载后才加上的,爬虫根本看不到
常见原因(按命中率排序)
1. 缺 twitter:card 声明
没有 <meta name="twitter:card">,X 根本不知道要渲染成哪种形状的卡片,结果要么回退到小型 summary,要么完全不渲染。这是「首次分享没卡片」最常见的原因。
怎么判断:对页面 view-source(不是 DevTools 的 Elements 面板——那看到的是 JS 跑完之后的 DOM),搜 twitter:card。原始 HTML 里没有就是它。
修复:至少输出
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="文章标题" />
<meta name="twitter:description" content="一句话摘要" />
<meta name="twitter:image" content="https://yourdomain.com/og/article.png" />
summary_large_image 是大多数内容站想要的 1.91:1 宽卡片;summary 是带小方图的紧凑卡。
2. twitter:image 用了相对路径
X 爬虫不会解析相对 URL。content="/og/foo.png" 直接被当字符串拿去 fetch,自然失败。
<!-- 错:相对路径 -->
<meta name="twitter:image" content="/og/article.png" />
<!-- 对:绝对 https URL -->
<meta name="twitter:image" content="https://yourdomain.com/og/article.png" />
怎么判断:view-source 看 content 属性,/ 或 ./ 开头都是错的。
3. 图不符合 X 的尺寸/格式要求
X 对 summary_large_image 有硬性要求(截至 2026 年 6 月):
| 字段 | 规则 |
|---|---|
| 格式 | PNG / JPEG / WebP(动图 GIF 可,但只取第一帧) |
| 最小尺寸 | 300 x 157 px |
| 最大尺寸 | 4096 x 4096 px |
| 最大文件大小 | 5 MB |
| 宽高比 | 1.91:1(偏离会被居中裁切) |
| 协议 | 只接受 HTTPS |
200x200 的 favicon、6MB 的大图、http:// URL 都会静默失败——卡片可能还会渲染,但图位是空的。
怎么判断:
curl -A "Twitterbot/1.0" -I "https://yourdomain.com/og/article.png"
# 期望:HTTP/2 200, content-type: image/png, content-length 小于 5000000
identify "https://yourdomain.com/og/article.png" # ImageMagick 确认尺寸
content-length 超 5000000 或尺寸不到 300x157 就重新导出。直接按 1200x630(1.91:1)设计,这样不会被裁。
4. X 缓存了旧版本(7 天缓存)
X 按 URL 缓存卡片数据,首次抓取后最长 7 天。你改完 meta 重发上线,但推文里还是旧图。2022/2023 年起 cards-dev.twitter.com/validator 已被关闭且一直没有替代品,没有公开的「强制重抓」按钮。
怎么判断:URL 粘到 opengraph.dev 或 metatags.io——它们会即时抓取,显示当前 meta 实际输出的卡片。若验证器显示新图但 X 还在显示旧图,就是 X 缓存。
修复(按有效性排序):
- URL 加个 cache-buster 参数:推
https://yourdomain.com/article/foo?v=2而不是裸 URL。X 按精确 URL 缓存,加了参数就被当成新页面。 - 等 7 天,缓存自然过期。
- 改 canonical URL(极少用,除非 slug 真换了)。X 是对重定向的最终落地 URL 做缓存,所以换一个 301 目标也会触发重抓。
5. 图在 auth / CDN 签名 / 防盗链后
twitter:image 指向 Cloudinary 签名 URL、需要 Referer 的 CDN,或登录墙后的路径。Twitterbot 拿到的是 401 / 403 / 302。
curl -A "Twitterbot/1.0" -I "https://cdn.yourdomain.com/og/article.png"
# 200 + image/png 表示 OK
# 401 / 403 / 302 表示被挡
修复:把 OG 图放进静态 public/ 目录,或在 CDN / WAF 上白名单 Twitterbot/1.0 user-agent。另外检查 robots.txt 里有没有 User-agent: Twitterbot 接 Disallow: /——这会悄悄把 X 挡在门外,而 LinkedIn、Slack(用的是别的爬虫)照常工作,这正是「哪儿都行就 X 不行」的典型特征。
6. meta 标签被客户端 JavaScript 注入(SPA)
X 爬虫不执行 JavaScript。如果你的 og: / twitter: 标签是 React Helmet、Vue Meta、客户端路由下的 next/head,或任何在客户端 hydrate 的框架在加载后才加上的,那爬虫抓到初始 HTML 时找不到卡片标签,就只渲染裸链接。这也是为什么 DevTools(看的是实时 DOM)里一切正常、卡片却还是出不来。
怎么判断:跑 curl -s "https://yourdomain.com/article/foo" | grep -i 'twitter:card\|og:image'。如果这些标签不在 curl 的原始响应里、却出现在浏览器 Elements 面板,那就是 JS 注入的。
修复:把 meta 标签改成服务端渲染或静态预渲染(Next.js/Nuxt/Astro 的 SSR 或 SSG,或框架的 metadata API),或用 prerender / 动态渲染给爬虫单独吐一份只含 meta 的 HTML。标签必须出现在最初那份 HTML 响应里,且在 <head> 内。
把 Open Graph 当 fallback(2026 年的实际行为)
截至 2026 年 6 月,X 在缺 twitter: 命名空间时会回退读 Open Graph。如果 og:image 设得对,你只要再加一行 <meta name="twitter:card" content="summary_large_image" /> 就够,可以省掉 twitter:image。内容站的推荐最小集:
<meta property="og:title" content="文章标题" />
<meta property="og:description" content="一句话摘要" />
<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" />
只有在你确实想让 X 用一张和 Facebook/LinkedIn 不一样的图时,才单独设 twitter:image。
最短修复路径
按命中率排序:
- 加
twitter:card(如果缺) -> 修掉占比最大的「没卡片」情形 - 把
twitter:image(或og:image)改成绝对 https -> 第二大 - 确认标签在服务端渲染的 HTML 里,而不是 JS 注入 -> SPA 的隐形杀手
- 图重新导出成 1200x630 PNG/JPEG,文件
< 1MB-> 修掉尺寸/格式问题 ?v=N强刷,或等 7 天 -> 旧图情形- CDN/WAF 白名单
Twitterbot、修robots.txt、把图移进public/-> 被挡图的长尾
怎么确认已经修好
- 像爬虫那样抓页面,确认标签在原始 HTML 里:
curl -s -A "Twitterbot/1.0" "https://yourdomain.com/articles/your-slug/" | grep -iE 'twitter:card|og:image' - 像爬虫那样抓图片,确认干净的
200:curl -A "Twitterbot/1.0" -I "https://yourdomain.com/og/your-image.png" - 把 URL 粘到 opengraph.dev——它会即时重抓,反映的是你当前的标签,而不是 X 的缓存。
- 最终真相:用一个小号发这条 URL(在跟旧缓存较劲就加
?v=2),确认大图真的渲染出来。
预防建议
- 统一用 1200x630 PNG 或 JPEG,目标
< 500KB,从主域名(不要签名 CDN)出。 - 用一个 helper 强制
og:image/twitter:image一定是带协议的绝对 URL。 - 确保卡片标签是服务端输出的,这样爬虫不跑 JavaScript 也能看到。
- CI 加检查:每篇新文章的 OG 图
curl -A "Twitterbot/1.0" -I确认200、content-type 正确、content-length 小于 5MB。 - 除非真要分平台用不同图,否则别设
twitter:image——让 X 回退读og:image,少一个字段就少一处会漂移的地方。 - OG 图按标题自动生成(
@vercel/og、satori、puppeteer),从源头杜绝「忘加图」。
FAQ
Q:X 老的卡片验证器死了,现在怎么测? A:用 opengraph.dev 或 metatags.io,都会即时抓你的 URL 并模拟 X 的渲染。要看绝对真相,用一个小号发一次然后截图。
Q:改完 meta 还是显示旧图,X 多久会重抓?
A:X 卡片缓存大约 7 天,没有公开的强制刷新。最快的 workaround 是推的 URL 加 ?v=2——X 会当成新 URL 立刻重抓。
Q:og:image 和 twitter:image 都要写吗?
A:不必。截至 2026 年 6 月,X 把 og:image 当 fallback 在读。除非要分平台用不同图,否则只写一个就够。唯一必须的是 <meta name="twitter:card">。
Q:DevTools 里标签明明是对的,卡片还是出不来?
A:DevTools 显示的是 JavaScript 跑完后的实时 DOM;X 爬虫不跑 JavaScript。用 curl -s <url> | grep twitter:card 看原始 HTML。如果 curl 里没有、DevTools 里有,那就是 JS 注入的,必须挪到服务端渲染的 HTML 里。
Q:卡片有图但裁切得很怪。
A:summary_large_image 强制 1.91:1。源图是 1:1 或 4:3 会被居中裁切。重新导出成精确的 1200x630。
Q:动图 GIF 只显示第一帧? A:是的,X 会拿第一帧定格。如果第一帧是黑底标题卡,卡片就像没图。直接用静态 PNG/JPEG。
Q:私信里 unfurl 正常,推文里不行。 A:私信预览是另一套即时抓取的管线,不走 7 天卡片缓存;推文渲染走卡片管线。两边表现不一致,就确认是缓存问题不是 meta 问题。