一、结构化数据不是加分项,它是「说清楚这是什么」
很多人对结构化数据的期待是「加了就有富摘要,加了排名就涨」。这个期待本身就是错的。结构化数据不会直接提升排名,它的作用是把页面上的内容用一种机器能确切理解的格式再说一遍:这是一个文章、作者是谁、发布于哪天、修改于哪天、这个页面的面包屑路径是什么、站点的站名是什么。搜索引擎拿到这份声明之后,才有可能在结果页里显示作者、日期、面包屑这些附加信息,也才有可能在遇到内容歧义时作出正确判断。
换句话说,结构化数据的收益是间接的:它降低了搜索引擎「猜错」的概率。这个价值在内容站上其实很大——同一个主题下有多个页面、同一篇文章被转载到几个不同的 URL、站名和域名不一致、作者信息散落在关于页里,这些情况都会让搜索引擎拿不准,而一份准确的 JSON-LD 能一次性说清。
但反过来,结构化数据也是最容易「加了却没用」的东西。原因几乎集中在四个地方:格式对但字段不对、字段对但值和页面内容不一致、标了但页面上没有对应内容、以及标了但被模板层转义坏了。下面按「写什么、写在哪、怎么验」三层来讲。
二、内容站真正值得标的四类,其余的可以先不做
Schema.org 里有两百多个类型,但一个内容站真正值得投入的只有四类,其余大部分标了也不会被使用。
第一类,Article 及其子类型。这是文章页的核心。注意用子类型比用 Article 本身更准确:技术教程用 TechArticle,新闻报道用 NewsArticle,博客文章用 BlogPosting。搜索引擎对子类型的处理会更细,虽然差异不大,但选择正确的类型本身没有额外成本。
第二类,BreadcrumbList。面包屑几乎是最值得标的一个,因为它不依赖内容质量,纯粹是结构信息,标对了就能在结果页里以层级形式展示,点击率提升是实打实的。而且它和站内的导航层级天然对应,几乎没有额外维护成本。
第三类,WebSite 和 Organization。这两个标在首页,用来声明站名(name)和站点搜索入口(potentialAction 里的 SearchAction)。站点名这一点很重要:Google 判断结果页里显示哪个名字作为站点名,靠的就是这个声明加首页上的 <title> 与 logo,不标的话它可能显示成域名的一串缩写。
第四类,Person。如果站点有明确的作者署名,把作者标成 Person,带 name、url(指向作者的介绍页)、sameAs(指向作者的社交主页或其它平台主页),这一份声明是「作者实体」的基础。对个人博客来说,这一项比 Article 里的 author 只写个字符串有价值得多。
不建议做的:Product、Recipe、Review 这些如果站里没有对应内容,标了属于「内容与标记不符」,是有风险的。FAQPage 曾经很值钱,但现在搜索结果里对常见问答的展示已大幅缩减,普通站点为它专门造一段问答内容,性价比不高。
三、一个最小可用的 Article 标记
JSON-LD 是首选格式,原因有三:它在 <script type="application/ld+json"> 里,不侵入 HTML 结构;它可以用 @graph 把多个实体写进同一个脚本块;它不依赖 DOM,模板里插一次就行,不用去给每个元素加属性。
一篇博客文章的最小标记:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": "文章标题,不超过 110 字",
"datePublished": "2026-11-14T09:30:00+08:00",
"dateModified": "2026-11-14T09:30:00+08:00",
"author": {
"@type": "Person",
"name": "作者名",
"url": "https://example.com/about.html"
},
"publisher": {
"@type": "Organization",
"name": "站点名",
"logo": {
"@type": "ImageObject",
"url": "https://example.com/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://example.com/123.html"
},
"image": "https://example.com/cover.jpg",
"inLanguage": "zh-CN"
}
</script>几个逐字要点。
headline 有长度限制,超过大约 110 个字符会被截断,中文按字数算大致是 55 字以内比较稳。这个字段不要和 <title> 完全绑定——页面的 <title> 常常为了 SEO 加后缀(「- 站点名」),而 headline 应该是纯粹的标题。
datePublished 必须带时区。2026-11-14 这种纯日期在某些解析器里会被当成 UTC 零点,导致时间偏移八小时,进而影响「新内容」的判定。写 2026-11-14T09:30:00+08:00 是标准做法。
dateModified 不能随手写成「当前时间」。很多模板直接输出 date('c'),等于告诉搜索引擎「这个页面每秒都在改」,这不仅失真,还会让搜索引擎忽略这个字段。它应该对应文章最后一次实质性修改的时间,改个错别字不算。
mainEntityOfPage 里的 @id 要写 canonical 那个 URL,不是当前请求的 URL。否则带参数访问的页面会生成一份 @id 不同的标记,等于自相矛盾。
image 建议给真实存在的封面图,宽高比接近 16:9,宽度不小于 1200 像素。很多富摘要的展示依赖这一项,不给就可能不展示。
四、用 @graph 一次声明多个实体,而不是塞一堆 script
文章页上除了 Article,通常还要标面包屑、可能的 FAQ、评论。用多个 <script> 块也行,但更清晰的做法是用 @graph 把多个实体放在一个块里,并用 @id 相互引用:
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "BlogPosting", "@id": "https://example.com/123.html#article",
"headline": "文章标题",
"breadcrumb": { "@id": "https://example.com/123.html#breadcrumb" },
"author": { "@id": "https://example.com/#author" } },
{ "@type": "BreadcrumbList", "@id": "https://example.com/123.html#breadcrumb",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "首页",
"item": "https://example.com/" },
{ "@type": "ListItem", "position": 2, "name": "技术教程",
"item": "https://example.com/tech/" },
{ "@type": "ListItem", "position": 3, "name": "文章标题" }
] },
{ "@type": "Person", "@id": "https://example.com/#author",
"name": "作者名", "url": "https://example.com/about.html" }
]
}用 @id 引用的好处是:同一个作者在多篇文章里可以是同一个实体,搜索引擎会把它聚合成一个「作者页」,长期来看这比在每篇文章里重复一段作者信息有价值得多。
面包屑的 itemListElement 里,最后一项不要写 item。最后一项代表当前页,页面自己不需要再链接自己,写了反而可能被判定为「面包屑最后一级带了链接」的不规范形态。前几项的顺序必须和页面上可见的面包屑完全一致,不一致会被判为「标记与页面内容不符」。
五、模板层落地:三种常见做法与它们的坑
落到具体的 CMS 上,通常有三种做法。
做法一,模板里手写 JSON。把上面那段 JSON 直接写进文章详情模板,把标题、日期、作者等位置换成模板变量。优点是完全可控,缺点是 JSON 里任何一个字符串如果包含双引号或反斜杠就会破坏整个块,导致整段 JSON 解析失败、所有标记一起失效。
正确做法是对每个插入的字符串做 JSON 转义,在 PHP 里就是用 json_encode($title, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),在 Go 模板里用 printf "%q" 或直接构造结构体再序列化。不要用 HTML 转义函数(htmlspecialchars)去处理 JSON 值——那会把引号转成 ",JSON 里的字符串内容就带上了 HTML 实体,标题显示成 "引号",比不标还糟。
做法二,用数组构造再 json_encode。在业务代码里拼一个 PHP 数组(或 Python dict),最后统一 json_encode。这是最稳的做法,因为转义交给序列化器处理,永远不会出现手工拼 JSON 的那种错误。Typecho、WordPress 的主题里都推荐这么写。
做法三,用插件或现成模块。WordPress 上有几个流行的结构化数据插件,优点是省事、覆盖的类型多;缺点是它们常常会同时输出好几套标记(插件一套、主题一套、SEO 插件一套),结果是同一页面出现三份 Article,字段互相矛盾。这种情况比不标更糟。用插件之前先 curl 一下页面数一数 application/ld+json 的块数。
无论哪种做法,有一个检查必须做:同一个页面只能有一份同类型的标记。文章页有且只有一个 BlogPosting,一个 BreadcrumbList。多份并存时搜索引擎通常取第一份,但你无法确定哪份在前。
六、验证:三个层次,缺一个都不算通过
验证这件事,很多人只做到「用富媒体测试工具跑一遍,显示无错误」就结束了。那只证明语法对,不证明它会被用上。完整的验证是三层的。
第一层,语法验证。把页面 HTML 里的 JSON-LD 抽出来,逐个 json.loads。这一步用脚本自己跑,比在线工具快得多,而且能批量:
python3 - <<'EOF'
import re, json, sys, urllib.request
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com/123.html'
html = urllib.request.urlopen(url).read().decode('utf-8', 'replace')
blocks = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)
print('blocks:', len(blocks))
for i, b in enumerate(blocks):
try:
d = json.loads(b)
types = [x.get('@type') for x in d.get('@graph', [d])]
print(f' [{i}] OK {types}')
except Exception as e:
print(f' [{i}] FAIL {e}')
EOF输出里 blocks 大于 2 就要警惕是不是重复标记;任何一个 FAIL 都说明整块失效。
第二层,字段与内容一致性。把标记里的 headline、datePublished、面包屑各级名称,和页面上肉眼可见的对应文字逐项比对。这一步自动化程度低,但它是「标记与内容不符」的唯一防线。常见的偏差有三种:标题被截断了(模板里用了 mb_substr 截到 30 字)、日期显示成「3 天前」而标记里是绝对日期(这没问题,但要确认绝对值是对的)、面包屑最后一级写的是栏目名而不是文章标题。
第三层,富媒体测试工具。Google 的 Rich Results Test 和 Schema Markup Validator 都要跑。前者告诉你「这个页面有没有资格出富摘要」,后者告诉你「字段有没有遗漏或类型错误」。两者结论常常不同:Schema Validator 通过,但 Rich Results Test 说「未检测到可展示的项目」,这通常意味着字段齐了但缺了展示必需的那一项(比如 image)。
三层都过之后,还有一个第四层,但它要等:在 Search Console 的「增强功能」报告里观察一到四周,看有没有出现「无效项目」的增量。新增的报错往往来自新发布的模板,而这批页面的问题在发布当时是发现不了的。
七、首页的 WebSite 标记与站点名
首页的那份标记单独说一下,因为它影响的是整个站点的展示名,而不是单篇文章。
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "站点名",
"url": "https://example.com/",
"inLanguage": "zh-CN",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://example.com/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}SearchAction 只有在站点真的有站内搜索且搜索结果页可以被访问时才应该标。标了但搜索功能不可用,属于声明了一个不存在的能力。另外 urlTemplate 里的参数名必须和真实搜索页的参数名一致——写 ?q= 而站点实际用 ?keyword=,这条标记就是错的。
站点名的判定不只看这一份标记,还看首页 <title> 里是否能清晰识别出站名、以及首页是否用一个 Organization 的 logo 声明了品牌图标。三处一致,站点名才算稳。如果这三处写的名字各不相同(比如标题里是「XX 的技术博客」,标记里是「XX 博客」,logo 上又是英文名),搜索引擎会挑一个它认为最合适的,通常不会是你想要的那个。
八、几个已经踩过的坑
坑一,把 JSON-LD 写在了 <body> 的中间。虽然规范允许,但部分解析器和缓存层对位置敏感。统一放在 <head> 末尾,位置固定,出问题好排查。
坑二,页面缓存把标记缓存成了另一个页面的。如果整页缓存的缓存键设计得不严谨(比如忽略了 URL 里的分页参数),可能出现列表页的缓存被当作文章页返回,标记就完全对不上了。这种情况在验证工具里表现为「标记里的 URL 和当前页面 URL 不一致」,看到这个报错先查缓存,别改标记。
坑三,author 写成了站点名。很多人图省事把作者填成站点名或「管理员」。这会被判定为无效作者,尤其是当同一作者署名了全站几千篇跨越多个领域的文章时,可信度会被打折。有真实作者就写真实姓名,宁可少标也不要乱标。
坑四,文章改版后忘了同步 dateModified。反过来,也有人每次都同步,包括改一个错别字。这个字段的价值在于「实质性更新」,频繁刷新等于告诉搜索引擎这个信号不可信。我自己的做法是:改动涉及正文内容或结论时更新,改错字、调格式、换配图不更新。
结构化数据这件事,投入产出比最高的做法是:把 Article 和 BreadcrumbList 在模板里做对一次,然后定期(每季度)抽查十个页面,确认没有因为模板改版而失效。它不需要持续投入,但需要一次做对。做得对的时候你几乎感觉不到它的存在,只有在结果页上出现了作者头像、面包屑路径、站点名的时候,才会想起它在那里起作用。