SPA 爬虫抓不到 Open Graph:SSR 与预渲染
你打开页面、打开 Elements,meta 都在:og:title、og:image、完整描述。同一条 URL 丢进 Slack 或 X,却是一条裸链接。别人问「是不是标签写错了」,你觉得明明写对了。
标签可以在浏览器里正确,对爬虫仍然不存在。这就是 SPA 的 Open Graph 陷阱。
用两次请求复现
先别和 DevTools 争论,先问:线上实际返回了什么。
# 社交爬虫大致拿到的:首包 HTML,不执行 JS
curl -sL -A "facebookexternalhit/1.1" https://example.com/app/product/42 | grep -iE 'og:|twitter:'
# 普通 UA(同样不执行 JS)
curl -sL https://example.com/app/product/42 | grep -iE 'og:|twitter:'
如果这两条 grep 是空的,平台不会从你 hydration 后的 DOM 里「变」出标签。查看网页源代码(不是 Elements 实时树)做的是同一件事:源码是响应体,Elements 是跑完 JS 之后的树。
| 你在看什么 | 代表谁 | 对分享卡片是否可信 |
|---|---|---|
| 浏览器 Elements / React DevTools | 登录后、执行完 JS 的真人 | 低 |
| 当前页上的浏览器扩展 | 已渲染 DOM(用户所见) | 验「人看到的」高,代替爬虫低 |
curl / 查看源代码 / 公网扫描 |
首包 HTML | 对爬虫高 |
| 平台官方 debugger | 该平台的缓存与抓取规则 | 对该平台权威 |
扩展读已渲染 DOM,很适合登录态、localhost、以及「编辑器有没有写对标签」。它不能代替爬虫视角。两层必须分开说,否则你会永远修错层。
为什么 CSR meta 能骗过开发者
客户端路由和 useEffect(或只在 mount 后跑的 helmet 类库)会在 hydration 之后往 document.head 写标签。人眼几乎看不出来:预览扩展、标签页标题、甚至「检查元素」都像已经完成。
多数社交爬虫不执行(或只很有限地执行)JavaScript。它们拉 HTML、解析 meta、缓存、走人。只靠 JS 才出现的标签,等于不存在。这和相对路径 og:image、爬虫被拦是同一类「卡片空白」,只是原因不同。五种空白拆法见为什么你的 Open Graph 卡片一片空白。
有用的二分法:
- 标签写错 —— 原始 HTML 里有字段,但值 / URL / 图有问题。改内容与图片链路。
- 爬虫根本拿不到 —— 字段只在 JS 之后出现(或只对非 bot UA 出现)。改投递:SSR、预渲染,或给可分享 URL 静态 HTML。
混在一起会浪费整周:你在打磨文案,爬虫拿到的仍是空壳。
框架对策(够用来选型)
不必打框架战争。你要的是:凡是别人会分享的 URL,首包 HTML 里就要有 meta。
| 做法 | 对 OG 意味着什么 | 常见场景 |
|---|---|---|
| SSR / 流式 SSR | 每次请求服务端写出 head | Next App Router metadata、Nuxt useSeoMeta、Remix loader |
| SSG / 构建期 HTML | 已知路径在文件里就有真实 meta | 营销页、文档、slug 已知的商品/文章 |
| 预渲染 / 渲染服务 | 对爬虫返回 HTML 快照 | 短期内重写不动的遗留 CSR |
| 边缘注入 / 改写 | CDN 对 bot 注入或替换 meta | 过渡方案;最终仍应回到应用内拥有标签 |
| 纯 CSR | 壳 + 客户端填充 | 后台壳可以;公开分享 URL 不行 |
实操里常踩的点:
- Next.js:
generateMetadata/ 静态 metadata 必须在该路由的服务端跑。只在客户端useEffect里改document.title修不好 OG。 - Nuxt 等:用框架自带、SSR 感知的 head API,让标签进响应体,而不是只进浏览器。
- Prerender 类服务与「动态渲染」:能在不大改架构时先让爬虫看见内容。那是基础设施与缓存策略,不是免检通行证——仍要用 curl 和官方 debugger 验收。
- 只对少数关键分享模板做构建期静态化,往往就够用。
若产品是登录后的纯管理后台、深链几乎不对外分享,这篇文章可能不是你的事故。若营销页、商品页或用户公开页天天被转发,纯 CSR meta 是结构问题,不是文案笔误。
没有 SSR 时,报告该怎么说
诚实的工具会分层。公网扫描若只看见首包 HTML:
- 200 成功但没有任何分享标签 → 应警告「标签可能只在客户端 JS 之后才有」(规则
og-tags-require-javascript),而不是武断写死「作者忘了og:title」。 - HTML 里有标签但图失败 → 图片工程问题,不是 SPA 投递问题。
- 扩展 DOM 快照有标签、原始 HTML 没有 → 双 HTML 证据已坐实:爬虫看不见(OGKit 在同时拿到两侧时会按错误级证据处理)。
工具做不到的事:替你设计 SSR 架构、迁移客户端路由、保证每个 bot 都执行你的 bundle。诊断止于「爬虫拿不到这些标签」。架构是下一场对话。
何时该找人做改造
证据清楚、改动是结构性的,再升级:
- curl / 查看源代码没有
og:*,hydration 后的应用有 - 成百上千条客户端路由 URL 都要可分享
- 单个静态
index.html壳列不全商品/文章路径 - 需要「预渲染过渡 + SSR/SSG 目标态」,而不是再装一个 meta 生成器
这类需求可以看咨询包:预渲染 / SSR——那是可选的人手,不是写出正确标签的前提。很多团队只要诊断够锐,自己改几个关键模板就够。
若只是缓存还在显示旧空壳,去 rescrape,不是上 SSR。若只是图片体积或重定向,去图片工程。走错分支会多耗一周。
清单:标签写错,还是爬虫瞎了
-
curl(或查看源代码)里的og:*/twitter:*与加载后一致 - 被分享的 URL 不是「document head 为空」的纯客户端路由
- Bot UA 没有被拦而浏览器正常(相关规则见 robots-blocks-social-crawlers)
- 没有把「仅扩展绿灯」当成 Facebook / LinkedIn / Slack 的验收
- 修好投递之后,对已缓存空壳的平台做了重新抓取
两个视角各试一次
- 把公网生产 URL 贴进首页扫描——爬虫视角的 HTML、命名规则、不碰本机权限。
- 只在登录后或 localhost 才存在的页面,用扩展扫当前页——那是已渲染 DOM。若公网扫描为空、扩展却是满的,SPA 缺口一张对比就成立。
OGKit 不会默默改写你的 React 树。它会标清你落在表的哪一侧:标签内容有问题,还是爬虫从来没机会看见。分清这一点,就是本文的全部工作。