连锁门店设施怎么讲给 AI 听:amenityFeature 与 LocationFeatureSpecification 的规范写法

2026-10-01 01:16:29 1 次浏览
GEOschema.org结构化数据JSON-LD门店数字化

上个月帮一家连锁洗车品牌(化名"车净邦",38 家门店)做官网改造,老板娘拿着手机来问:为什么用户在 AI 搜索里问"附近哪家洗车店有充电桩",AI 推荐的全是隔壁街的竞对?她家门店其实三公里内就有五个充电位。我打开她家门店页源码一看——设施信息全写在一张图片里,AI 爬虫抓到的 HTML 里一个字都没有。这事儿其实不难修,schema.org 早就有现成的字段,只是大多数连锁品牌从来没正经填过。这篇就把 amenityFeature 和 LocationFeatureSpecification 这对字段的规范写法拆开讲透,属于生成式引擎优化(Generative Engine Optimization, GEO)里门店类目最基础也最容易写错的一环。

amenityFeature 到底是个什么字段

先说结论:amenityFeature 是 schema.org 里 Place 类型的一个属性,专门用来描述一个场所提供哪些设施。停车场、无障碍通道、Wi-Fi、充电桩、母婴室,都归它管。而 LocalBusiness、Store 这些门店常用的类型,全部继承自 Place,所以你的门店结构化数据天然就能挂这个属性。

连锁门店地图与设施图标的主题插画

麻烦在于它的取值类型:amenityFeature 要求的值是 LocationFeatureSpecification。这个类型是 PropertyValue 的子类型,也就是说它不是简单的字符串,而是一个"名字-值"对的结构体。三个核心字段要搞清楚:

  • name:设施名,推荐直接用 schema.org 给出的枚举建议或行业惯用英文(如 parking、wheelchairAccessible、EVCharging),可以配中文说明;
  • value:设施的具体取值,可以是布尔、文本或数字;
  • unitText:当 value 是数字时,说明单位,比如 个、kW、小时。

三种典型写法放一张表里对照:

设施场景 name 建议写法 value 写法 unitText 是否需要
免费停车场 parking true 不需要
5 个充电桩 EVCharging 5 需要(个)
120kW 直流快充 EVChargingPower 120 需要(kW)
无障碍通道 wheelchairAccessible true 不需要
停车 3 小时内免费 parking 免费3小时(文本描述) 不需要

注意第三行和最后一行的区别:带数量、带功率的写法比一句"有充电桩"信息密度高得多,AI 在做设施比较时依赖的就是这些可计算的值。模糊的"有/无"只能进候选集,具体的数字才能进排序。

挂在 LocalBusiness 或 Store 下的嵌套写法

门店页面一般用 Store(零售)或 LocalBusiness 的具体子类(如 AutoWash、Restaurant)。amenityFeature 直接作为它的属性写进 JSON-LD。下面这段 Python 脚本是我给车净邦批量生成 38 家门店 JSON-LD 用的,从 CSV 里读门店数据再拼结构化数据,你们抄去改改字段就能用。

依赖:Python 3.10+,标准库即可,无第三方包;环境:任意能跑 Python 的 CI 或本机。

import csv
import json

# 门店 CSV 字段约定:shop_id, name, address, lat, lng, parking, ev_count, ev_power_kw
# 设施字段统一收口到 build_store_node,页面渲染层只负责把 JSON-LD 原样输出
def build_store_node(row: dict) -> dict:
    # 门店节点用 Store 类型,零售连锁比泛用 LocalBusiness 更精确
    # geo 与 address 建议同时给,AI 做距离计算时优先读 geo
    node = {
        "@type": "Store",
        "name": row["name"],
        "address": {
            "@type": "PostalAddress",
            "streetAddress": row["address"],
            # addressCountry 用 ISO 3166 两位码,官方推荐写法
            "addressCountry": "CN",
        },
        "geo": {
            "@type": "GeoCoordinates",
            # 坐标保留 5 位小数即可,够 POI 级精度
            "latitude": round(float(row["lat"]), 5),
            "longitude": round(float(row["lng"]), 5),
        },
        "amenityFeature": [],
    }

    # features 先攒在临时列表里,最后一次性挂到节点上
    features = []

    # 停车场:布尔型设施,value 直接用 Python 布尔,序列化后是 true/false
    # 注意不要写成字符串 "true",部分消费方会当作文本匹配失败
    if row["parking"] == "1":
        features.append({
            "@type": "LocationFeatureSpecification",
            # name 用小写英文短语,与 schema.org 惯例保持一致
            "name": "parking",
            # True 序列化为 true,符合 JSON Boolean 规范
            "value": True,
        })

    # 充电桩数量:数字型设施,必须带 unitText,否则 AI 无法判断单位
    # 数字先转 int,避免 CSV 读进来是 "5" 这种字符串
    if row["ev_count"]:
        features.append({
            "@type": "LocationFeatureSpecification",
            "name": "EVCharging",
            "value": int(row["ev_count"]),
            # unitText 建议用简短中文或国际单位,别写长句
            "unitText": "个",
        })

    # 快充功率:同样是数字型,功率是有业务含义的过滤条件
    # 用户问"有没有快充",AI 就是拿这个字段比对的
    if row["ev_power_kw"]:
        features.append({
            "@type": "LocationFeatureSpecification",
            "name": "EVChargingPower",
            "value": int(row["ev_power_kw"]),
            # kW 是功率的国际单位符号,不要写成"千瓦"
            "unitText": "kW",
        })

    # 无障碍通道:布尔型,schema.org 官方示例的标准写法
    # 即便门店没有这个设施,也建议显式写 value 为 false
    features.append({
        "@type": "LocationFeatureSpecification",
        "name": "wheelchairAccessible",
        "value": True,
    })

    # 攒完统一挂载,保持 node 构造和设施拼装解耦
    node["amenityFeature"] = features
    return node

def main():
    stores = []
    with open("stores.csv", encoding="utf-8") as f:
        # DictReader 按表头取列,CSV 列顺序变了也不会错位
        for row in csv.DictReader(f):
            stores.append(build_store_node(row))
    # 每个 @type 节点单独输出一份 JSON-LD,嵌进对应门店页的 head 里
    # ensure_ascii=False 保证中文原样输出,爬虫读到的就是可读文本
    for s in stores:
        with open(f"ld/{s['name']}.json", "w", encoding="utf-8") as out:
            # @context 必须指向 https://schema.org,缺了整份数据不生效
            json.dump({"@context": "https://schema.org", **s}, out, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    main()

整体嵌套关系画成图是这样的:

flowchart TD
    A["@context: https://schema.org"] --> B["@type: Store(门店节点)"]
    B --> C["name / address / geo / telephone"]
    B --> D["openingHoursSpecification(营业时间)"]
    B --> E["amenityFeature(设施列表)"]
    E --> F["LocationFeatureSpecification #1<br/>name=parking, value=true"]
    E --> G["LocationFeatureSpecification #2<br/>name=EVCharging, value=5, unitText=个"]
    E --> H["LocationFeatureSpecification #3<br/>name=wheelchairAccessible, value=true"]

最容易犯的错是把设施写成一串逗号分隔的字符串塞进某个自定义字段,比如 "facilities": "停车,充电桩,WiFi"。字符串没法被消费方逐条解析,AI 抽取时只能整段当文本,粒度全丢了。一条设施一个 LocationFeatureSpecification 节点,这是规范底线。

value 是布尔值时的几个坑

value 接受布尔值是 schema.org 的正式定义,"value": true 没问题。但实际落地有三个坑,都是我们在车净邦项目里一步步踩出来的。

第一个坑:个别老解析器和 CMS 模板会把布尔序列化成字符串 "True"(首字母大写),Strict JSON 解析会直接报 Expecting value: line 12 column 30 这种错。统一用小写 true,生成侧用 json.dumps 而不是手拼字符串,就能避开。

第二个坑:布尔型设施表达不了"有条件"。比如"停车免费但仅限消费顾客",这时 value 就该换成文本 "消费满50元免费停车2小时"。布尔表达有/无,文本表达条件,别硬用一个布尔去压缩语义。

第三个坑:时间敏感型设施。充电桩晚上十点后关闭、停车场营业到几点,可以给 LocationFeatureSpecification 挂 hoursAvailable 属性进一步限定,这是官方支持的写法,比在 value 里写"白天可用"这种模糊文本规范得多。

value 类型 适用设施 例子 要不要 unitText
Boolean 有/无型 parking: true 不要
Number 数量/规格型 EVCharging: 5 要
Text 条件型 消费满额免费停车 不要

和地图平台 POI 设施字段的口径差异

很多团队以为把地图 POI 填好了就完事,其实 schema.org 和地图平台的设施字段是两套口径,谁也替代不了谁。

对比项 地图平台 POI(高德/百度/腾讯) schema.org amenityFeature
设施表达 平台预设的标签枚举,选勾为主 自由 name/value 对,可带数量与单位
精细度 "有充电站"级别的开关 能写到"5 个桩、120kW"级别
更新方式 走平台审核,周期以天计 自己页面自己改,即时生效
消费方 地图 App 内展示与导航 网页爬虫、AI 搜索引擎、语音助手
条件描述 基本不支持 文本 value 可写清楚

差异的本质是消费方不同:地图 POI 喂的是地图 App 自己的展示层,schema.org 结构化数据喂的是网页侧的爬虫和 AI 抓取管线。两边都填、口径对齐,才不会出现"地图上写着有充电桩、AI 一问说没有"的精分现场。

AI 回答「附近哪家店有充电桩」的机制与原理

把原理搞明白,才知道为什么这些字段值得认真填。AI 搜索回答"附近哪家洗车店有充电桩",大致走这样一条链路:

sequenceDiagram
    participant U as 用户
    participant AI as AI 搜索引擎
    participant IDX as 抓取与索引层
    participant W as 门店页(JSON-LD)

    U->>AI: 附近哪家洗车店有充电桩?
    AI->>IDX: 解析意图(品类=洗车, 设施=充电桩, 范围=附近)
    IDX->>W: 召回候选门店页,读取 amenityFeature
    W-->>IDX: EVCharging value=5 unitText=个
    IDX->>IDX: 过滤(无该设施的页淘汰) + 排序(数量/距离/权威度)
    IDX-->>AI: 候选门店及设施证据
    AI-->>U: 给出推荐门店与引用来源

关键在第 4 步和第 5 步:没有结构化设施字段的页面,抓取层只能对整页文本做模糊匹配,"充电"两个字可能出现在促销文案里而不是设施描述里,置信度上不去就进不了候选集。这就是 GEO(让内容被 AI 引用推荐)和传统 SEO 最大的分岔点——传统搜索看关键词命中,AI 搜索看实体属性是否明确、可计算、可引用。车净邦改完结构化数据两周后,我们站内日志看到 GPTBot 和 PerplexityBot 对门店页的抓取频次涨了三倍多,这个数字是我们自己日志统计的,不代表普遍规律,但方向是对的。

一次真实的排错记录

顺手把车净邦项目里卡了最久的一个问题记下来,你们大概率也会遇到。上线第一周,有个站点的结构化数据检测工具一直报 amenityFeature 类型不识别。查了半天,是 CMS 模板转义出了问题:@type 被模板引擎渲染成了 @type 的 HTML 实体,JSON-LD 块里的引号也被转成了中文引号。修复方式是把 JSON-LD 从模板字符串改成后端渲染前生成好、直接以 <script type="application/ld+json"> 原样输出,不经任何 HTML 转义管线。改完当天检测就过了。

另外我们的做法是每次发布前跑一个校验脚本,解析页面里所有 JSON-LD,逐个检查 amenityFeature 节点的 @type、value 类型和 unitText 配套是否合规,报错就挡发布。这层门禁不贵,但省的返工真不少。

下面是那个校验的核心片段,Node.js 写的,跟上面的 Python 生成脚本配套。

依赖:Node.js 18+,无第三方依赖;环境:CI 流水线或本地任意终端。

const fs = require("fs");

// 校验单个门店 JSON-LD:设施节点类型、value 类型、unitText 配套
// 用法:node validate_amenity.js ld/某门店.json,失败时退出码非 0
// 只校验设施相关字段,地址坐标等交给富结果测试工具
function validateAmenity(node) {
  const errors = [];
  // amenityFeature 缺失不报错,但存在时逐条检查
  if (!Array.isArray(node.amenityFeature)) return errors;

  // entries() 拿到索引,报错信息里带上第几条,排查快
  for (const [i, f] of node.amenityFeature.entries()) {
    // 每条设施必须是 LocationFeatureSpecification 类型
    if (f["@type"] !== "LocationFeatureSpecification") {
      errors.push(`第${i}条 @type 不合法: ${f["@type"]}`);
    }
    // name 必填,空名字的设施节点等于白写
    if (!f.name || typeof f.name !== "string") {
      errors.push(`第${i}条 name 缺失`);
    }
    // value 必须是布尔、数字或字符串之一,缺 value 直接报错
    const ok = ["boolean", "number", "string"].includes(typeof f.value);
    if (!ok) {
      errors.push(`第${i}条 value 类型不合法: ${typeof f.value}`);
    }
    // 数字型 value 必须带 unitText,否则单位语义丢失
    if (typeof f.value === "number" && !f.unitText) {
      errors.push(`第${i}条 数字 value 缺 unitText`);
    }
    // 文本 value 建议写具体条件,别用"可用"这种模糊表述
  }
  return errors;
}

// 读取生成脚本输出的 JSON 文件并跑校验,有错误就以非零码退出,挡住 CI
// argv[2] 是文件路径参数,CI 里传生成目录下的单个 JSON 即可
const stores = JSON.parse(fs.readFileSync(process.argv[2], "utf-8"));
// 校验失败打印全部错误再退出,别只报第一条,省得改一轮跑一轮
const errs = validateAmenity(stores);
if (errs.length > 0) {
  console.error("amenityFeature 校验失败:");
  for (const e of errs) console.error(" - " + e);
  process.exit(1);
}
console.log("amenityFeature 校验通过");

这套"生成 + 校验"两段式是门店结构化数据最省心的维护姿势:设施变更只改数据源,格式合规交给门禁兜底。

结尾说说误区

最后澄清两个常见误区。有人觉得 amenityFeature 在 Google 的富结果文档里没被列为必需字段,就断定它没用——那个文档说的是富搜索结果(星星级、门店卡片)的要求,而 AI 搜索引擎抓取结构化数据并不以"能否触发富结果"为准,设施属性属于它理解门店实体的基础素材。另一个误区是只在官网首页写一份全门店通用的设施描述,38 家店有 38 种设施配置,通用描述等于没说,一家店一份、跟着门店页走才算数。

往后 AI 搜索对线下门店的推荐权重只会更高,设施这类"机器可验证的属性"会比营销文案值钱。你在门店结构化数据上踩过什么坑,评论区聊聊。

参考与延伸

  • schema.org amenityFeature 属性定义:https://schema.org/amenityFeature
  • schema.org LocationFeatureSpecification 类型定义:https://schema.org/LocationFeatureSpecification
  • schema.org LocalBusiness 类型定义:https://schema.org/LocalBusiness
  • Google 搜索中心 本地商家结构化数据文档:https://developers.google.com/search/docs/appearance/structured-data/local-business

关键词:GEO、AI优化AIO、amenityFeature、LocationFeatureSpecification、schema.org、LocalBusiness、门店AI推荐、结构化数据

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