导语:一个 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字段作用
WebSitename、url、description声明站点名与站点身份,供搜索引擎理解"这个站叫什么"
Organizationname、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 统计生产构建
可解析块数072 个块全部 JSON.parse 通过作者自述
线上现状04 处 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 条配置动作:

  1. 结构化数据一律用原生 <script type="application/ld+json"> 输出,放在 root layout 或页面级服务端模板里;next/script、标签管理器、useEffect 注入都不是它的正确归处。

  2. 验证口径固定在 curl。凡是"抓取方要读的东西",就以服务器返回的字节为准,生产构建为准。DevTools 和在线验证器都会执行 JS,它们和爬虫看到的世界不一致。

  3. 把检查写进 CI:grep -c 'application/ld+json' 为 0 就让构建失败,紧跟一步对每个块做 JSON.parse。这个断言比任何人工复查都便宜。

2 条红线:

  1. 别用 strategy="afterInteractive" 承载 JSON-LD。它不会报错、不会警告、DevTools 里还看得到,属于典型的静默失败:你付出的成本全在"以为已经上线"。

  2. 别把"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