创见博客
JSON-LD :让AI/搜索引擎更容易读懂你的页面
七崽爱吃小饼干2026/08/24阅读 2

打开一篇文章时,人可以从标题、作者、日期和正文排版中判断它是一篇博客文章。但爬虫看到的首先是一棵 DOM 树:一个 h1、几个 span、一些链接和大量文本。它可以推断这些内容的含义,却不一定能稳定判断谁是作者、哪个时间是发布时间,以及网站和发布组织是什么关系。

JSON-LD 解决的正是这个问题。

html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "从页面到实体:前端开发者的 JSON-LD 实战",
  "author": {
    "@type": "Person",
    "name": "作者名称"
  }
}
</script>

这段代码不会渲染任何界面,也不会执行一段业务脚本。它只是用机器可读的方式说明:当前内容是一篇博客文章,它的标题是什么,作者是谁。

JSON-LD,全称是 JSON for Linking Data。

1. JSON-LD 是什么

JSON-LD 是一种使用 JSON 表达关联数据的格式。这里的“关联”很关键:它不仅描述字段,还描述现实世界中的实体及其关系。

例如,一个内容网站至少包含三类实体:

  • WebSite:网站本身。
  • Organization:运营或发布网站的组织。
  • BlogPosting:网站中的某一篇文章。

它们不是三段互不相关的数据。文章属于网站,网站由组织发布,文章也有自己的作者。JSON-LD 可以通过稳定的 @id 把这些实体连接起来:

text
BlogPosting
  ├── author ──────> Person
  ├── publisher ───> Organization
  └── isPartOf ────> WebSite

因此,JSON-LD 更接近一份“页面内容说明书”,而不是页面正文的另一份副本。

2. JSON-LD 有什么作用

JSON-LD 最直接的作用是降低机器理解页面的成本。

一篇文章的页面上可能同时出现发布时间、更新时间、评论时间和推荐文章时间。如果只读取 HTML,爬虫需要结合标签、样式和上下文进行推断。通过 datePublished 和 dateModified,页面可以直接声明两个时间的语义。

类似地,JSON-LD 可以明确表达:

  • 当前页面是网站、文章、商品还是作者主页。
  • 页面标题、摘要、图片和主要语言是什么。
  • 作者是一个 Person,发布者是一个 Organization。
  • 当前文章属于哪个网站。
  • 页面在面包屑导航中的位置。

搜索引擎可以用这些信息理解实体、校验页面信息,并判断页面是否具备特定搜索展示形式的资格。生成式搜索和其他 AI 系统也可能利用结构化数据辅助理解页面,但这里要保持一个边界:JSON-LD 不保证排名,也不保证内容一定被 AI 引用。

HTML 正文仍然是内容主体。JSON-LD 负责解释内容,不能替代内容。

可以把几个常见文件的职责分开理解:

技术主要职责
HTML提供用户可见的正文和页面结构
JSON-LD解释页面中的实体、属性与关系
robots.txt声明爬虫可以访问哪些路径
sitemap.xml提示站点有哪些可发现页面
Canonical声明页面的规范 URL

3. Schema.org 是什么

Schema.org 是一套用于描述网页实体的公共词汇表,由 Google、Microsoft、Yahoo 和 Yandex 在 2011 年共同发起。它不是一种数据格式,也不是某个搜索引擎私有的 API,而是一份约定:网站、文章、人物、商品等实体应该叫什么,每类实体可以使用哪些属性,以及不同实体之间如何建立关系。

例如,Schema.org 将网站定义为 WebSite,将组织定义为 Organization,将博客文章定义为 BlogPosting。对于 BlogPosting,它又定义了 headline、author、datePublished、image 等属性。不同网站只要使用同一套词汇,搜索引擎就不必分别猜测每个字段的业务含义。

JSON-LD 和 Schema.org 经常一起出现,但两者职责不同:

名称负责什么
JSON-LD规定如何用 JSON 表达实体、标识和关系
Schema.org规定实体类型和属性使用什么统一名称

可以把 JSON-LD 理解为“语法”,把 Schema.org 理解为“词汇表”。@context: "https://schema.org" 的含义,就是告诉解析器:接下来的 BlogPosting、headline 和 author 等词汇,应按照 Schema.org 的定义解释。

Schema.org 提供的是通用词汇,不等于所有字段都会触发搜索结果增强展示。具体平台支持哪些类型、要求哪些字段,还需要查看 Google Search Central 等平台文档。

4. 字段是怎么定义的

我们通常写在一个对象里的字段,实际上来自两套规范。

第一套是 JSON-LD。它定义数据如何组织,常见关键字都以 @ 开头:

字段含义
@context当前数据采用哪套词汇表
@type当前实体属于什么类型
@id实体的稳定、全局标识
@graph在同一段数据中描述多个实体

第二套是 Schema.org。它定义可以描述哪些实体,以及实体有哪些属性。例如:

  • WebSite 可以使用 name、url、inLanguage 和 publisher。
  • Organization 可以使用 name、url、logo 和 sameAs。
  • BlogPosting 可以使用 headline、author、datePublished 和 dateModified。
  • BreadcrumbList 通过 itemListElement 描述导航层级。

Schema.org 的类型存在继承关系。BlogPosting 继承自 Article,Article 又继承自 CreativeWork,因此它也能使用上层类型定义的 name、description、image 等属性。

Schema.org 本身相对宽松,不代表每个字段都会被所有搜索平台采用。落地时需要同时查看三层要求:

  1. JSON-LD 语法是否合法。
  2. Schema.org 是否允许该类型使用这个属性。
  3. Google、Bing 等具体平台是否支持该类型,以及哪些字段是必需或推荐字段。

不要随意创造 myCustomField 之类的属性。JSON 语法虽然允许,但不在 Schema.org 词汇表中的字段通常不会被识别。

schema.org中Article介绍的截图

5. Next.js 首页实现

首页适合描述网站和运营组织。在创见博客中,我们使用一个 @graph 同时放入 Organization 与 WebSite:

tsx
const siteUrl = (
  process.env.NEXT_PUBLIC_SITE_URL || "https://visionaryblog.cn"
).replace(/\/$/, "");

const homeJsonLd = {
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": `${siteUrl}/#organization`,
      name: "创见博客",
      url: siteUrl,
      logo: {
        "@type": "ImageObject",
        url: `${siteUrl}/logo.svg`,
      },
      description: "面向开发者的技术内容分享与交流平台",
    },
    {
      "@type": "WebSite",
      "@id": `${siteUrl}/#website`,
      url: siteUrl,
      name: "创见博客",
      description: "面向开发者的技术内容分享与交流平台",
      inLanguage: "zh-CN",
      publisher: {
        "@id": `${siteUrl}/#organization`,
      },
    },
  ],
};

这里有两个容易忽略的细节。

第一,@id 是实体标识,不一定是一个需要单独访问的页面。https://visionaryblog.cn/#organization 表示“该网站对应的组织实体”。只要全站稳定使用这个标识,文章页就能通过相同的 @id 引用它。

第二,publisher 没有重复填写整个组织对象,而是通过 @id 指向前面定义的 Organization。这就建立了“网站由该组织发布”的关系。

在 App Router 的服务端页面中,可以直接输出脚本:

tsx
export default async function Home() {
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(homeJsonLd).replace(/</g, "\\u003c"),
        }}
      />
      <HomeClient />
    </>
  );
}

application/ld+json 表示脚本内容是关联数据,不是可执行 JavaScript。.replace(/</g, "\\u003c") 则避免动态内容中的 < 被浏览器当成 HTML 边界,是输出 JSON-LD 时值得保留的安全处理。

6. Next.js 文章页实现

文章详情页不能复用一份固定对象。标题、作者、标签和时间都应来自当前文章数据,因此 JSON-LD 应在服务端按文章动态生成。

先处理 URL 和日期:

tsx
const toAbsoluteUrl = (value: string) => {
  if (/^https?:\/\//i.test(value)) return value;
  return `${siteUrl}/${value.replace(/^\//, "")}`;
};

const toIsoDate = (value: string) => {
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
};

结构化数据中的图片应该使用绝对地址。日期则推荐输出 ISO 8601 格式,例如 2026-08-24T08:30:00.000Z。

接着生成 BlogPosting:

tsx
const getArticleJsonLd = (article: ArticleDto) => {
  const articleUrl = `${siteUrl}/reader/${article.id}`;

  return {
    "@context": "https://schema.org",
    "@graph": [
      {
        "@type": "BlogPosting",
        "@id": `${articleUrl}/#article`,
        headline: article.title,
        description: article.summary || article.content.slice(0, 120),
        url: articleUrl,
        mainEntityOfPage: {
          "@type": "WebPage",
          "@id": articleUrl,
        },
        image: article.cover
          ? toAbsoluteUrl(article.cover)
          : undefined,
        datePublished: toIsoDate(article.published_time),
        dateModified: toIsoDate(article.updated_time),
        inLanguage: "zh-CN",
        keywords: article.tags?.length ? article.tags : undefined,
        author: {
          "@type": "Person",
          "@id": `${siteUrl}/userCenter/${article.author_id}/article#person`,
          name: article.author_nickname,
          url: `${siteUrl}/userCenter/${article.author_id}/article`,
        },
        publisher: {
          "@type": "Organization",
          "@id": `${siteUrl}/#organization`,
          name: "创见博客",
        },
        isPartOf: {
          "@type": "WebSite",
          "@id": `${siteUrl}/#website`,
          name: "创见博客",
          url: siteUrl,
        },
      },
    ],
  };
};

这段对象表达了几层关系:

  • 当前 URL 对应一篇 BlogPosting。
  • mainEntityOfPage 表示文章是当前网页的主要实体。
  • author 指向一个具体作者。
  • publisher 复用首页定义的组织实体。
  • isPartOf 表示文章属于创见博客网站。

文章页还可以在同一个 @graph 中加入面包屑:

tsx
{
  "@type": "BreadcrumbList",
  "@id": `${articleUrl}/#breadcrumb`,
  itemListElement: [
    {
      "@type": "ListItem",
      position: 1,
      name: "首页",
      item: siteUrl,
    },
    {
      "@type": "ListItem",
      position: 2,
      name: article.title,
      item: articleUrl,
    },
  ],
}

最后,在服务端取到文章后输出:

tsx
const article = await getPublishedPublicArticle(articleId);
if (!article) notFound();

const jsonLd = getArticleJsonLd(article);

return (
  <NavLayout>
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
      }}
    />
    <ArticleReaderContent article={article} />
  </NavLayout>
);

服务端输出比在 useEffect 中注入更稳妥。爬虫取得初始 HTML 时就能读取结构化数据,也不会因为客户端请求失败而缺失关键字段。

7. 不要把 JSON-LD 写成隐藏内容仓库

JSON-LD 中的信息应当与页面可见内容一致。

如果页面显示作者是“张三”,结构化数据却声明作者是“李四”,机器无法确认哪份信息可信。类似问题还包括:

  • 页面没有评分,却在 JSON-LD 中填写五星评价。
  • 内容没有更新,只为了制造新鲜度修改 dateModified。
  • 页面没有问答区域,却批量生成 FAQPage。
  • 商品已经无货,结构化数据仍然声明 InStock。

结构化数据的价值来自准确性,而不是字段数量。与其把所有可能字段都填上,不如只输出能够从真实业务数据中确认的字段。

8. 最后

JSON-LD 不是一段神秘的 SEO 配置。对前端开发者来说,它本质上是把已经存在于页面和数据库中的业务实体,用统一词汇重新表达一遍。

首页声明“我是谁”,文章页声明“这是什么内容、由谁创作、属于哪个网站”。当 HTML、Metadata、Canonical、Sitemap 和 JSON-LD 对同一事实保持一致时,机器理解页面就不再完全依赖猜测。

真正值得追求的不是“每个页面都有一段 JSON-LD”,而是每段结构化数据都准确描述了当前页面。

评论
0/100