导语:一个 Next.js 站点给首页打了 WebSite 与 Organization 两段 JSON-LD,验证器全绿、DevTools 里也能看到,但 view-source 数出来是 0 个。原因只有一个:用了 next/script,结构化数据被推迟到客户端水合之后才注入。改回原生 <script> 后,每页 4 处 ld+json,其中两个块可直接解析,@type 分别是 WebSite 与 Organization。
背景:所有检查都说"没问题"
how-we-invest.com 是一个用 Next.js 做的 13F 持仓披露追踪站,把机构投资者的申报文件整理成可查的索引。作者遇到的问题不是"标记写错了",而是"站点在 Bing 上几乎没有收录",回头查结构化数据时才发现更底层的问题。
当时的实现长这样,写在 root layout 里,全局生效:
import Script from 'next/script'
export default function Layout({ children }) {
return (
<body>
<Script
id="website-structured-data"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
{children}
</body>
)
}这段代码读起来完全合理:组件是对的、类型是对的、内容也是对的。但它有个不会报错的默认值:next/script 的 strategy 默认是 afterInteractive,标签在水合完成之后才注入客户端 DOM。而 JSON-LD 不是要"执行"的脚本,它是必须随首屏 HTML 一起发出去的内容。
于是出现了最难查的一类问题:服务端发出的 HTML 里一个 JSON-LD 都没有,但你能想到的验证方式全都显示正常。
DevTools 的 Elements 面板能看到标签。因为那是水合之后的实时 DOM。
Rich Results Test 和各类 Schema 校验器大多跑无头浏览器,会执行 JS,也能看到标签。
代码本身没有任何可疑之处。
唯一不显示的地方是 view-source:,也就是服务器真正发出去的字节,而这恰好是绝大多数抓取方读的东西。
落地清单
1. @type 与字段
站点级两个实体,都是 Google 文档里明确支持的用法:
| @type | 字段 | 作用 |
|---|---|---|
| WebSite | name、url、description | 声明站点名与站点身份,供搜索引擎理解"这个站叫什么" |
| Organization | name、url、description、foundingDate | 声明背后的组织实体,做实体消歧 |
这两块就是修复后线上实际存在的标记,2026-11-05 直接从首页 HTML 里取,未做改写:
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "How We Invest",
"url": "https://how-we-invest.com",
"description": "An index of what investors wrote and what they filed, from primary sources."
}{
"@context": "https://schema.org",
"@type": "Organization",
"name": "How We Invest",
"url": "https://how-we-invest.com",
"description": "Independent research indexing investor writing and institutional filings for individual investors.",
"foundingDate": "2024"
}必须说清楚这块的短板:两段都没有 logo,Organization 也没有 sameAs。按 Google 的 Organization 文档,这两项都不是必填,但 logo 影响知识面板与结果里的图像,sameAs 是把 LinkedIn、Crunchbase、Wikidata 等站外档案绑到同一个实体上的唯一字段。这是一个能过验证、但没吃满收益的标记,属于下一步该补的部分。
2. 插在哪个节点、怎么注入
修法只有一行:不再用 next/script,改成原生 <script>,让 React 直接把它渲染进服务端 HTML。
export function JsonLd({ data }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
)
}没有 import、没有 strategy、也不需要 id。位置仍是 app 的 root layout,因此全站每一页都会带上。
作者在文章里补了一个他自己也被纠正过的点:这种做法在 client component 里同样有效。'use client' 的含义是"还要水合",不是"不参与服务端渲染",首次渲染照样输出到 HTML 里。这个细节决定了你能不能把 JsonLd 组件放在任意需要的地方,而不用为了 SSR 把整棵组件树改成 server component。
3. 验证:用 curl,不用 DevTools
排查时的那一行命令,直接数服务器返回的 HTML 里有没有 ld+json:
curl -s https://example.com/ | grep -c 'application/ld+json'
修复前每页返回 0。修复后要在生产构建上验,不能用 dev server:
next build && next start curl -s http://localhost:3000/ | grep -c 'application/ld+json'
整站一次性核对构建产物:
find .next/server/app -name '*.html' -exec sh -c \
'echo "$(grep -c "application/ld+json" "$1") $1"' _ {} \;最后一步是把每个块真正解析一遍,防止转义没做好、发出去的是坏 JSON:
const html = fs.readFileSync(file, 'utf8')
for (const m of html.matchAll(/<script type="application\/ld\+json">(.*?)<\/script>/gs)) {
JSON.parse(m[1]) // 解析失败会直接抛错
}作者跑完这一步:72 个块全部通过解析。他自己说,这两分钟的检查避免了一次"带着坏转义上线"。
2026-11-05 复检线上首页:HTML 中出现 4 处 application/ld+json 字符串,其中 2 个块可完整解析,@type 分别为 WebSite 与 Organization。也就是说,文章中描述的"0"已经是历史状态。
结果:前后对照
| 指标 | 修复前 | 修复后 | 口径 |
|---|---|---|---|
| 首屏 HTML 中的 JSON-LD | 每页 0 个 | 每页 2 到 5 个 | 作者自述,用 curl + grep 统计生产构建 |
| 可解析块数 | 0 | 72 个块全部 JSON.parse 通过 | 作者自述 |
| 线上现状 | 0 | 4 处 ld+json 出现,2 个块完整可解析 | 本站 2026-11-05 直取首页 HTML 复检 |
| Bing 收录 | 作者描述为"几乎没有收录" | 未给收录数字 | 定性,无量化 |
| 富媒体展现 / CTR | 无数据 | 无数据 | 本文不推断 |
这里刻意不写"修完收录就上来了":作者没有给收录曲线,标记进 HTML 只是必要条件,不是充分条件。
一个很有说服力的外部基线,来自第三方对 92 家 Forbes Cloud 100 公司首页的抓取(2026-10-28):79% 的首页有 JSON-LD,74% 用 Organization 标记公司,48% 有 WebSite;另有 19 家首页完全没有 JSON-LD,其中包括 Anthropic、Perplexity、Hugging Face。该研究在方法说明里明确写了一条限制:用 JavaScript 注入的标记不算数,因为只解析服务器返回的 HTML。
这条限制正好解释了本次案例为什么值得写:一个页面如果靠水合注入标记,在外部审计、Bing、社交卡片抓取器、以及一大批不执行 JS 的 LLM 爬虫眼里,它和"根本没做结构化数据"是完全一样的。你在自己浏览器里怎么刷新都看不出差别。
复盘
3 条配置动作:
结构化数据一律用原生
<script type="application/ld+json">输出,放在 root layout 或页面级服务端模板里;next/script、标签管理器、useEffect注入都不是它的正确归处。验证口径固定在 curl。凡是"抓取方要读的东西",就以服务器返回的字节为准,生产构建为准。DevTools 和在线验证器都会执行 JS,它们和爬虫看到的世界不一致。
把检查写进 CI:
grep -c 'application/ld+json'为 0 就让构建失败,紧跟一步对每个块做 JSON.parse。这个断言比任何人工复查都便宜。
2 条红线:
别用
strategy="afterInteractive"承载 JSON-LD。它不会报错、不会警告、DevTools 里还看得到,属于典型的静默失败:你付出的成本全在"以为已经上线"。别把"Rich Results Test 通过"当成"已经发布"。它跑的是无头浏览器,会执行 JS;标记只在客户端存在时,它依然可能给你绿灯。同理,不要用浏览器插件或 Elements 面板的结果去回答"爬虫能不能读到"。
标注
@type:WebSite、Organization(站点级两件套)。
Schema.org 版本:现行版,查询于 2026-11-05。本次网络无法访问 schema.org/version/latest,未读到具体版本号,故按"现行版"标注,不写推测的版本数字。
验证工具与时间:curl + grep 统计、逐块 JSON.parse(作者,2026 年内);本站直取首页 HTML 复检,2026-11-05;作者另用 Google Rich Results Test 做过交叉验证。
第三方数据:Forbes Cloud 100 首页结构化数据研究,样本 92 家可读首页,抓取日 2026-10-28,来源 flux.la,属第三方自述统计,本站未复现。
来源:https://dev.to/saikodev/your-nextjs-json-ld-might-be-invisible-to-crawlers-heres-a-one-line-check-2nbb ;站点实测 https://how-we-invest.com/ 。
标签
Schema.org、结构化数据、JSON-LD、AI可见度、GEO、Next.js、WebSite