Hexo 博客加 JSON-LD 结构化数据: 让搜索引擎读懂你的文章
前言
之前写过三篇 Hexo SEO 系列(搜索引擎原理、站内优化、提升排名), 讲的多是 meta 标签、关键词、内外链这些。
但有一个东西漏了 —— 结构化数据。
它的作用是把”这是一篇文章, 作者是谁, 什么时候发的, 封面图是哪张”这类信息, 用机器能读的格式告诉搜索引擎。好处很直接: 搜索结果里可能直接显示出封面图、作者名、发布时间, 点击率比纯文字结果高。
这篇记录怎么接。
原理: 什么是 JSON-LD
三种格式, 选 JSON-LD
结构化数据有三种写法:
| 格式 | 写法 | 推荐度 |
|---|---|---|
| Microdata | 写在 HTML 属性里 | 一般, 侵入性强 |
| RDFa | 写在 HTML 属性里 | 一般 |
| JSON-LD | 单独一段 <script> |
推荐 |
选 JSON-LD 的理由: 与 HTML 完全分离。 写在 HTML 属性里的方案, 改模板容易把结构化数据改坏; JSON-LD 是一段独立的代码, 不动页面结构, 维护成本最低。
长什么样
1 | <script type="application/ld+json"> |
搜索引擎会解析这段 JSON, 提取成知识图谱里的实体。
第一步: 加到 Butterfly 主题里
Butterfly 提供了 inject 机制, 可以往页面里塞代码。
先说清楚这个版本的 inject 支持哪些位置。 我查了本地主题源码, 实际只有两个注入点:
| 位置 | 作用范围 | 对应模板 |
|---|---|---|
head |
所有页面的 <head> 里 |
layout/includes/head.pug |
bottom |
所有页面底部(</body> 前) |
layout/includes/additional-js.pug |
源码里就这两行:
1 | // layout/includes/head.pug |
这意味着没有”仅文章页”的注入点。 网上很多教程写的 inject.post / inject.post-bottom 在这个版本里不生效 —— 配了也不会被渲染。
那文章级的 JSON-LD 怎么加?
既然没有文章级注入点, 就用 head, 但在代码里自己判断当前是不是文章页。
1 | inject: |
page.post 在文章页上存在, 在独立页面、分类页、标签页上都是 undefined, 用它判断最直接。
注意 inject 的格式: 源码里是
data.join(''), 所以每一项必须是一个字符串, 不能写成{ code: 'xxx' }这种对象结构。写成对象会渲染成[object Object]。
顺带说明: Pug 模板里的条件语法
Butterfly 用 Pug, 但 inject 的内容是字符串, 会被当成普通文本输出, 不会被 Pug 编译。
所以上面的 <% if (page.post) { %> 这种 EJS 风格语法, 在 inject 里同样不会被解析 —— 它会原样输出到页面上。
正确做法是改用主题的标签系统, 或者接受所有页面都注入。
考虑到这一点, 更实用的方案是: 给所有页面都注入 JSON-LD, 但用不同的 @type 区分。搜索引擎能接受一个页面里有多个 JSON-LD 块, 它会按类型分别理解:
1 | inject: |
如果你的站点没有独立页面(关于页等), 或者独立页面很少, 直接全站注入 BlogPosting 也没有大碍。 独立页面通常就那么几个, 搜索引擎不会因为一两个页面的类型标注不精确就惩罚整个站点。
另一个更稳的方案: 直接改主题模板。 主题的
layout/post.pug是 Pug 文件, 里面可以用真正的 Pug/JS 语法判断页面类型。代价是主题升级时可能要重新改(所以要先改themes/butterfly还是用extend/inject覆盖, 取决于你愿不愿意接受维护成本)。
第二步: 动态取值
上面那段 JSON 里的字段不能写死 —— 每篇文章的标题、时间都不一样。得用模板变量动态填充。
先说个坑: inject 里不能用模板变量
这是最容易踩错的地方。 很多人以为 inject 里能写 <%- title %>, 实际上不行。
看主题源码里 inject 的实现:
1 | // scripts/helpers/page.js |
只是 join(''), 没有任何模板编译。 意味着你在 inject 里写的 <%- title %> 会原封不动地作为文本输出到 HTML 里, 页面上会出现字面量 <%- title %>。
结论: 需要动态取值的结构化数据, 不能靠 inject, 必须改主题模板。
正确做法: 在 post.pug 里加
Butterfly 的 layout/post.pug 是真正的 Pug 模板, 里面可以用完整语法。
在 layout/post.pug 的 block content 下面找合适位置(建议放在 article#article-container 之后), 加:
1 | if page.title |
注意 Pug 里输出 JSON 用了 script(type="application/ld+json"). 这个写法 —— 末尾的点是 Pug 的”纯文本块”语法, 后面跟着的内容原样输出, 不做 HTML 转义。
这一点很关键: 如果用 script(type="application/ld+json")= #{json}, Pug 会把引号转义成 ", JSON 就废了。
几个关键点
标题字段用 headline 不是 name
文章类型用 headline。用 name 会被 Google 判为”不匹配预期类型”。
时间要用 ISO 8601 格式
page.date.toISOString() 输出的是 2026-10-21T03:05:42.000Z, 这是标准格式。
不要直接输出 page.date 本身, 默认的 2026-10-21 11:05:42 这种格式搜索引擎可能解析失败。
URL 要拼成绝对路径
#{config.url}/#{page.path} 拼出的是完整 URL, 搜索引擎最喜欢这种。
如果用 url_for(page.path), 得到的是带 config.root 的站内路径, 部署在根目录时也能用, 但部署在子目录时可能拼不完整。所以这里直接用 config.url 更稳。
封面图要有兜底
不是每篇文章都设了封面。所以要准备 fallback:
1 | "image": "#{page.cover || config.site.img || config.site.logo}" |
注意 page.cover 可能是相对路径(比如 /images/xxx.png)。搜索引擎对相对路径的 image 支持不稳定, 稳妥起见拼成绝对 URL:
1 | "image": "#{page.cover && !page.cover.startsWith('http') ? config.url + page.cover : (page.cover || config.site.img || config.site.logo)}" |
注意 cover 可能是相对路径, 而结构化数据的 image 最好是绝对 URL。如果搜索引擎抓不到完整 URL, 富媒体展示就不会生效。稳妥起见可以这样写:
1 | "image": "<%- (cover && cover.startsWith('http')) ? cover : (config.root + (cover || config.site.img || config.site.logo)) %>" |
第三步: 验证
写完不能靠猜, 必须用官方工具验。
Google 富媒体测试工具
地址: https://search.google.com/test/rich-results
输入你的文章 URL, 点测试。它会告诉你:
- 结构化数据是否被识别
- 哪些字段有警告或错误
- 预览效果长什么样
Schema Markup Validator
地址: https://validator.schema.org/
比 Google 那个更宽松, 主要用来检查 JSON 语法和 schema 定义是否符合规范。两个都跑一遍, 前者看实际效果, 后者查语法细节。
本地怎么验
本地还没部署的话, 可以直接看渲染后的 HTML:
1 | hexo clean && hexo g |
然后在 public/ 里找文章页面:
1 | # 找到刚生成的文章 |
确认 JSON 语法没错:
1 | # 提取 JSON 部分并校验格式 |
常见陷阱
陷阱一: JSON 里的引号没转义
这是最常见的错误。如果你的标题里带了英文双引号, JSON 就会解析失败, 整段结构化数据直接作废。
比如标题是 Hexo 配置的 "url" 该怎么填, 直接塞进 JSON 就崩了。
解决办法: 在模板里做 JSON 转义
Pug 的纯文本块(script(...).)不做 HTML 转义, 也就不会帮你转义 JSON 里的双引号。所以要在模板里手动处理。
在 post.pug 里定义一个变量先转义:
1 | - var ldTitle = (page.title || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"') |
两个转义都要做: 先转义反斜杠 \, 再转义双引号 "。顺序反了会出问题 —— 先转义双引号的话, 新产生的 \ 会被二次处理。
description 和 keywords 同理, 有引号的话也要处理。
更省事的办法: front-matter 里的标题和描述尽量用中文标点(「」『』””), 避开英文双引号。但这只能规避, 不是根治。
陷阱二: 和主题自带的重复
Butterfly 5.x 本身没有内置 JSON-LD(我查过 layout/ 和 scripts/, 没有 ld+json 相关代码), 所以这块不会撞车。
但 canonical 和 og:url 这类标签, 主题和 SEO 插件只能有一个来源。
检查方法: 看渲染后的 HTML 里有没有多个同类标签。
1 | grep -c "application/ld+json" public/xxxxx.html |
大于 1 就要去掉多余的。
陷阱三: 忘了写 dateModified
datePublished 和 dateModified 都应该填。
datePublished—— 文章首次发布时间dateModified—— 最后修改时间
老文章更新后, dateModified 是重要的信号。 只写 datePublished 会让搜索引擎以为这篇内容一直没变过, 不给重新抓取的权重。
你可以在 front-matter 里加 updated 字段:
1 |
|
陷阱四: 忘记改 updated 字段
上面那个 updated 不会自动更新, 得手动维护。
Hexo 有个插件可以自动根据 Git 提交时间生成, 但更简单的方式是每次改文章时顺手更新 updated 字段。
陷阱五: 结构化数据被当成作弊
不要在结构化数据里塞关键词。 比如把 keywords 写成 "Hexo,Hexo教程,Hexo博客,Hexo搭建,hexo主题", 这种堆砌反而会被判为垃圾信号。
keywords 字段就填真实的标签, 有几个填几个, 没有就干脆不填。
可以加的其他类型
BlogPosting 之外, 常见的还有:
| 类型 | 用途 |
|---|---|
WebSite |
站点级信息, 全站通用 |
Person |
作者信息 |
BreadcrumbList |
面包屑导航 |
FAQPage |
问答页面(注意: Google 已限制该类型的富媒体展示) |
HowTo |
步骤教程类文章 |
加站点级 WebSite
WebSite 的字段所有页面都一样(站点名、站点 URL), 所以这条可以放心用 inject 注入 —— 不需要动态取值, 也没有引号转义问题。
1 | inject: |
站点名和 URL 写死就行 —— 站点信息本来就不会每篇文章变。
不过要注意: 只有在页面真的有搜索功能时, 声明 SearchAction 才有意义, 否则 Google 会提示这个动作无效。
你如果开了 Butterfly 的搜索功能(search.enable: true), 可以加上:
1 | "potentialAction": { |
关于改主题模板的维护成本
前面说了要改 themes/butterfly/layout/post.pug。这会带来一个问题: 主题升级时这个改动会丢失。
两个应对办法:
办法一: 改之前留个备份
1 | cd themes/butterfly/layout |
升级主题后对比 diff, 把改动重新补回去。
办法二: 用 Hexo 的主题覆盖机制
Hexo 支持通过 _config.yml 里的 theme 字段配合 extend 生成器做覆盖, 但 Butterfly 官方文档对此支持有限。
更实用的第三种: 把 JSON-LD 的生成逻辑写成一个自定义 Hexo 过滤器或脚本, 输出到 source/ 下, 再用 inject 引入。这样升级主题不受影响。
不过说实话, 对大多数个人博客来说, 办法一就够用了 —— 一年升级主题一两次, 每次补一下也花不了几分钟。改之前记一下改了什么就行。
小结
接入结构化数据, 核心就三步:
- 加代码 —— 文章级改
themes/butterfly/layout/post.pug, 站点级用inject.head - 动态取值 —— Pug 里用
page.xxx, 时间转 ISO 8601, URL 拼绝对路径, 引号手动转义 - 用官方工具验证 —— Google 富媒体测试 + Schema Markup Validator
最需要小心的三个点:
- inject 里不能用模板变量 —— 它只做
join(''),<%- title %>会原样输出 - JSON 里的双引号要转义 —— 先转
\, 再转", 顺序不能反 - 别和主题自带的标签重复 —— canonical、og:url 这类一个来源就够了
结构化数据是纯增量的 —— 加错了顶多不起作用, 不会让搜索排名变差。所以值得花半小时配好它, 换来的是搜索结果里更丰富的展示。
参考链接:
💡 准备购买云服务器 / 域名来搭建或部署站点?
通过下面的链接下单,通常能领到新用户专属优惠,也能小额支持本站持续更新:
阿里云(返现) |
腾讯云 2核2G 99元/年 |
腾讯云 云产品特惠












