导航菜单也能给 AI 指路:SiteNavigationElement 与 significantLink 的独立站实战

2026-09-25 01:20:15 1 次浏览
GEOSiteNavigationElementAI爬虫JSON-LD独立站前端SEO

九月初我们排查一个客户站(化名“老陈的五金站”,主营手动工具,SKU 一万二左右),发现一个怪事:Perplexity 回答“best torque wrench for bike repair”时,引用了他们博客,却把整条扭矩扳手产品线漏掉了。老陈原话是:“博客文章都收录了,产品页也收录了,怎么 AI 就是不推我的类目?”查了一晚上日志才反应过来——问题出在导航菜单上。整个品类导航是 JS 折叠面板加一张雪碧图,AI 爬虫拿到 HTML,只看到十几个 <div> 和 alt 为空的 <img>,站点的类目结构对它来说等于不存在。

这篇就把这次导航语义化改造(Navigational Semantic Refactor)的过程拆开讲:改什么、怎么改、抓取日志里发生了什么变化。

折叠菜单为什么读不懂:AI 爬虫的抓取机制

先说底层原理。Googlebot 渲染能力强,Chrome 142 内核,JS 跑完再解析 DOM,折叠菜单展开后照样能抓。但 AI 引擎的抓取器不是这么干活的。PerplexityBot、GPTBot、ClaudeBot 这类爬虫为了控制成本,多数请求走的是“拿原始 HTML 就走”的模式,执行 JavaScript 的比例很低,而且是异步、限速的。我们日志里 8 月 26 日到 9 月 2 日这一周,GPTBot 一共来了 41 次,带 JS 渲染的请求是 0 次。

网站导航语义化指路 AI 爬虫

这意味着什么?你的站点结构信息,AI 引擎只能从两个地方拿到:原始 HTML 里的链接文本,以及结构化数据。折叠菜单把链接藏在 click 事件后面,图片导航把链接文本变成像素——两条路全堵死。AI 引擎理解不了“这个站有 6 大品类、下面 30 个子类目”,它眼里的站点就是一堆孤立的页面。推荐的时候自然漏品类,因为它根本不知道品类存在。

生成式引擎优化(Generative Engine Optimization, GEO)里有个共识:给 AI 的东西要放在最便宜的解析路径上。原始 HTML 是最便宜的,JSON-LD 次之,渲染后的 DOM 最贵。导航恰恰是站点结构最浓缩的表达,把它埋在最贵的路径里,等于白费劲。

改造方案:三层补齐

我们没有推翻现有 UI,设计师坚持折叠交互不动。改造分三层,互相兜底:

层 手段 解决的问题 改动量
语义 HTML 导航链接改为服务端直出的 <nav><ul><li><a>,CSS 控制折叠 原始 HTML 可见 前端 2 天
JSON-LD 首页注入 SiteNavigationElement,深层页补 significantLink 明确告诉 AI“这是站点结构” 后端模板 1 天
可访问性 加 aria-label 与跳转锚点 次要收益,顺带修 半天

核心思路是:同一个导航,给浏览器一套 UI,给爬虫一份语义,两份内容必须一致。别想着给 AI 一套隐藏菜单、给用户另一套——我们 2024 年在另一个站试过 cloaking 式的“AI 专用导航”,三个月后被 Google 降权,教训刻骨铭心。

第一步:把链接从 JS 里捞出来

原来的菜单是点击后 JS 往容器里塞链接。改造后,服务端直接渲染完整的 <ul> 结构,折叠只是 CSS 类切换。这是最关键的一步,不管后面 JSON-LD 写得多漂亮,这一步不做都是空中楼阁。

@* 环境:ASP.NET Core 8 Razor,导航分部视图 _NavMenu.cshtml *@
@model IEnumerable<CategoryNode>
@{
    // 服务端直出完整类目树,折叠只靠 CSS 切换,绝不依赖 JS
    // 一级品类按上季度 GMV 降序,最多取 6 个进菜单
    // 空壳品类(在售 SKU 为 0)不进导航,免得稀释结构
    // Slug 已统一为小写加连字符,结尾斜杠由模板补齐
    // 链接文本必须与 JSON-LD 里的 name 逐字一致
    // 别在这里做任何 UA 判断,一套输出服务所有访问者
    // 改动上线前用 curl 模拟 GPTBot UA 抓一遍确认可见
    // 每个链接的 href 都要能被无渲染爬虫直接解析
    // 上线后第二天抽查日志,确认爬虫开始抓子类目页
    var topCats = Model
        // 过滤掉在售 SKU 为 0 的空壳品类
        .Where(c => c.LiveSkuCount > 0)
        .OrderByDescending(c => c.QuarterGmv)
        .Take(6)
        .ToList();
}
<nav aria-label="Product categories">
  <ul class="nav-tree">
    @foreach (var cat in topCats)
    {
      <li>
        <a href="/@cat.Slug/">@cat.DisplayName</a>
        @if (cat.Children.Any())
        {
          <ul class="nav-sub">
            @foreach (var sub in cat.Children)
            {
              // 子类目全量直出,爬虫顺着 href 能抓到完整层级
              <li><a href="/@cat.Slug/@sub.Slug/">@sub.DisplayName</a></li>
            }
          </ul>
        }
      </li>
    }
  </ul>
</nav>

CSS 侧用 :focus-within 和 :has() 做展开,键盘和爬虫都不需要 JS 就能触达全部链接。这里有个坑:原来为了动画流畅,子菜单用了 display:none + JS 延迟插入。改成纯 CSS 后动画没了,设计师闹了两天情绪,最后用 grid-template-rows 的过渡技巧补回来了。动画可以妥协,链接可见性不能妥协。

第二步:SiteNavigationElement 标注

schema.org 的 SiteNavigationElement 类型,很多教程只教你在每个链接上单独标。实测更稳的做法是:一个 <script type="application/ld+json"> 里放一个节点,用 hasPart 把整棵导航树挂上去。

<!-- 环境:首页模板内联注入,内容与导航 HTML 一一对应 -->
<!-- hasPart 嵌套别超过两层,实测过深会被部分 AI 解析器忽略 -->
<!-- url 必须与导航 href 逐字一致,含结尾斜杠,混写等于白写 -->
<!-- name 与导航链接文本一致,不要做同义词替换 -->
<!-- 建议全文只放一个导航 JSON-LD 节点,多处重复会互相覆盖 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "SiteNavigationElement",
  "name": "Main Navigation",
  "hasPart": [
    { "@type": "SiteNavigationElement", "name": "Torque Wrenches", "url": "https://www.example-tools.com/torque-wrenches/" },
    { "@type": "SiteNavigationElement", "name": "Socket Sets", "url": "https://www.example-tools.com/socket-sets/" },
    { "@type": "SiteNavigationElement", "name": "Hand Tool Storage", "url": "https://www.example-tools.com/hand-tool-storage/" }
  ]
}
</script>

注意两点。一是 hasPart 嵌套层级别太深,我们实测超过两层,部分 AI 引擎的解析器直接忽略深层节点,保持两层平铺最省事。二是 url 必须和导航 HTML 里的 href 逐字一致,包括结尾斜杠。老陈站上 /torque-wrenches 和 /torque-wrenches/ 混用,第一版 JSON-LD 就这么写的,Rich Results 测试通过,但语义对不上号,等于白写。

第三步:significantLink 补“页面内部的重要出口”

significantLink 是另一个容易漏的武器。它的语义是“这个页面指向的、对理解页面重要的资源”,最典型的用法是品类页指向子品类和核心购买指南。首页导航解决的是“站点有什么”,品类页的 significantLink 解决的是“这个品类下有什么值得看”。

@* 环境:品类页模板,Razor 输出子品类与指南的出口列表 *@
@{
    // significantLink 只挑对理解品类结构有价值的出口
    // 子品类加购买指南,总数控制在 8 到 12 个
    // 分页链接、筛选参数页塞进来会稀释语义,实测过
    // 输出顺序保持稳定,别每次请求随机排序
    // URL 全部用完整域名地址,相对路径部分引擎不认
    var sigLinks = Model.SubCategories
        // 按子类目在售 SKU 数降序,砍掉尾部小类目
        .OrderByDescending(s => s.SkuCount)
        .Take(8)
        .Cast<object>()
        // 指南页最多带 4 篇,挑转化稳定的头部那批
        .Concat(Model.BuyingGuides.Take(4));
}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "CollectionPage",
  "name": "@Model.CategoryName",
  "significantLink": [
    @foreach (var link in sigLinks)
    {
      <text>"@link.AbsoluteUrl"</text>
    }
  ]
}
</script>

这里我们踩过一个度的问题:一开始把筛选参数页也塞进 significantLink,一个品类页挂了 40 个链接。后来砍到 8-12 个,只留子品类和指南。significantLink 不是越多越好,它是给 AI 的摘要,不是站点地图。

改造前后的抓取路径变化

光讲道理没用,看数据。以下全部是我们自建日志监测的口径(Nginx access log + 每日脚本聚合),不是任何第三方工具的统计。

指标(周期均为改造前 3 周 vs 改造后 3 周) 改造前 改造后 变化
GPTBot 周均请求(HTML 不带渲染) 41 63 +54%
PerplexityBot 命中 /torque-wrenches/ 次数 2 11 +9 次
AI 引擎来源会话(周均) 17 46 +170%
AI 推荐提到品类名的次数(人工抽检 50 次) 4 19 明显改善

最直观的验证方法是把问题丢回给 AI 引擎。9 月 18 日用 Perplexity 问同一个问题,回答里第一次出现了 "example-tools' torque wrench lineup (click-style, digital, and beam types)"——品类线结构被完整复述了,这在改造前从来没发生过。

抓取路径的变化,机制上一张时序图就能说清:

sequenceDiagram
    participant B as GPTBot
    participant H as 首页(原始HTML)
    participant C as 品类页
    B->>H: GET / (不执行JS)
    H-->>B: nav直出的ul/li/a + SiteNavigationElement
    B->>B: 建立6大品类30子类目结构图
    B->>C: 顺着结构补抓品类页
    C-->>B: significantLink列表(子品类+指南)
    B->>B: 记录品类下值得看的资源

还有一条隐藏收益:结构清楚了之后,AI 引擎在站内的抓取深度也变了。改造前 GPTBot 的请求里 87% 落在首页和博客,改造后落到品类页的占比升到 34%。结构图一旦建立,爬虫会顺着结构去补全它没见过的分支,这是导航语义化最被低估的作用。整条链路可以这样概括:

flowchart TD
    A[AI 爬虫请求首页] --> B{原始 HTML 中导航是否可读}
    B -->|改造前 JS 折叠+图片导航| C[只能拿到正文链接]
    C --> D[站点结构=未知]
    D --> E[推荐时整条产品线缺席]
    B -->|改造后 nav 直出+JSON-LD| F[读到 SiteNavigationElement]
    F --> G[建立品类与子类目的结构图]
    G --> H[品类页 significantLink 补充深度]
    H --> I[推荐时能按类目引用]

Nginx 侧的两个小补丁

改造不是只有模板。我们顺手在 Nginx 层做了两件事,配合度很高。

第一,给 AI 爬虫 UA 返回的 HTML 不做任何区别——这条是红线,前面说过 cloaking 的教训。但可以做的区别是性能:AI 爬虫不带缓存、每页全量拉,大站会被打疼。给这些 UA 单独开一个轻量的 fastcgi 缓存池,5 分钟过期,内容完全一致,纯粹省机器。

# 环境:Nginx 1.24,AI 爬虫轻量缓存池
# 只有缓存性能不同,返回的 HTML 一个字节都不许变,防 cloaking
map $http_user_agent $ai_bot_group {
    default             0;
    ~*GPTBot            1;
    ~*PerplexityBot     1;
    ~*ClaudeBot         1;
    ~*Google-Extended   1;
}
# 打标之后日志里按 $ai_bot_group 聚合,周报脚本直接可用
# 缓存区 5 分钟过期,够挡全量拉取又不至于太陈旧
# 再配一条 fastcgi_cache 分区映射,命中 ai_light 组走轻量缓存
# 灰度期间先只对 PerplexityBot 开,观察两周再全量
# fastcgi_cache_path /var/cache/nginx/ai_light levels=1:2 keys_zone=ai_light:50m inactive=5m;

第二,日志里给这几个 UA 打标,方便周报聚合。没这个,你连“AI 爬虫来了多少次、看了什么”都说不清,GEO 这事儿就全靠感觉。我们现在的周报脚本 40 行 bash,每周一早上自动跑,省事得很。

没解决的问题,照实说

改造三周,有两个问题还在观察。

一个是 Bing 系的反馈慢。ChatGPT 的搜索底座深度依赖 Bing 索引,而 Bing 对我们站点结构化数据的更新周期肉眼可见地长,9 月 20 日提交了 IndexNow,到写这篇时品类结构在 Copilot 的回答里还没体现。这个只能等。

另一个是 significantLink 的权重到底有多高,说实话没测出来。我们做了一次 A/B:一半品类页带 significantLink,一半不带,两周后 AI 引用的差异在噪声范围内。样本太小,可能要跑到 Q4 才有结论。所以现在我们把它当“低成本的正向押注”维护着,不吹它是决定性因素。

最后提醒一句:导航语义化是让 AI 看懂你的结构,前提是你的结构本身合理。6 个品类藏 30 个子类目没问题,要是 40 个平级品类挤在一个菜单里,AI 读懂了也记不住。先理顺信息架构,再做语义标注,顺序别反。

参考与延伸

GEO · AI搜索 · SiteNavigationElement · significantLink · JSON-LD · 独立站 · AI爬虫

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。