复制一份 Product 的 JSON-LD 模板,换成自己的产品名和图片,价格那一栏先留着——很多询价型产品页的标记就是这么来的。模板里 "price": "0" 没删干净,或者为了「看起来完整」补了一句 "availability": "https://schema.org/InStock"。页面上写着「询价」,标记里是 0 元现货。
Search Console 的结构化数据报告对这类问题不总是报错:有时候报「价格无效」,有时候什么都不报,页面只是始终拿不到价格那一条富媒体结果。把哪些字段能省、哪些不能编弄清楚,比追着报错改省事。
schema.org 的要求和 Google 的要求不是一回事
schema.org 定义词汇,Google 定义它愿意为什么样的标记给展示位。一个 Product 只要给出 name,在 schema.org 层面就合法,offers、review、aggregateRating 都是可选项。
Google 的 Product 结构化数据分成两类体验:
- product snippet(产品摘要):搜索结果里带产品名的那几条,
name是必需的,offers、review、aggregateRating属于加强项; - merchant listing(商家商品列表):带价格、库存、配送信息的那种,必须有
offers,里面要有price(或priceSpecification)和priceCurrency,通常还要availability、image、url。
询价型产品页没有公开价格,走不了第二类。这不算标记写坏了,是这页本来就不在 merchant listing 的适用范围里。硬凑一个价格塞进去,换来的是标记与可见内容不一致——Google 的结构化数据政策把这类行为归到误导性标记,属于可以人工处理的范围。
先确认这页该不该用 Product
用一句话确认页面性质:客户在这一页能下单,还是只能提交询价?
- 只能询价、卖的是实物型号 →
Product,省略offers; - 卖的是服务(安装、代运营、技术咨询)→
Service,用serviceType、provider、areaServed,不要套 Product; - 型号列表页、分类页 →
ItemList加BreadcrumbList;别在列表页给每个型号都塞一份完整 Product,容易和详情页的@id打架。
没有价格时的一份完整写法
下面这份能过 Schema Markup Validator 的语法校验:
{
"@context": "https://schema.org",
"@type": "Product",
"@id": "https://example.com/products/px-300#product",
"name": "PX-300 工业压力变送器",
"url": "https://example.com/products/px-300",
"image": ["https://example.com/img/px-300-front.jpg"],
"description": "量程 0–10 MPa,4–20 mA 两线制输出,G1/2 外螺纹,用于液压站与试验台的压力监测。",
"sku": "PX-300",
"mpn": "PX-300",
"brand": {
"@type": "Brand", "name": "Example" },
"additionalProperty": [
{
"@type": "PropertyValue", "name": "量程", "value": "0–10 MPa" },
{
"@type": "PropertyValue", "name": "输出信号", "value": "4–20 mA" },
{
"@type": "PropertyValue", "name": "过程连接", "value": "G1/2 外螺纹" },
{
"@type": "PropertyValue", "name": "最小起订量", "value": "10 件" }
],
"potentialAction": {
"@type": "QuoteAction",
"target": "https://example.com/contact?sku=PX-300"
}
}
几个决定值得说清楚。
offers 整个不要出现,也不要留一个空对象或者 "price": "0"。"price": "0" 在解析方看来就是零元,不是「面议」。有人改用 "priceSpecification": { "@type": "PriceSpecification", "price": "0" } 表达同样意思,结果一样。
availability 不确定就别写。写了 InStock 而页面上根本没有库存说明,这是第二处不一致。
additionalProperty 承载的是询价页真正有价值的那部分信息:型号、量程、材质、公差、认证、MOQ 都往这里放。PropertyValue 的 name 和 value 都是自由文本,不会被翻译、不会被格式化,写客户在页面上看到的那句话就行。它是规格的容器,不表达优惠。
potentialAction 配 QuoteAction 只是语义信号,Google 不拿它做富媒体结果,加不加都能过校验。我倾向加上,因为至少把「这一页的动作是询价」写进了机器可读的部分。target 直接指向带型号参数的询价表单,比指向首页有用。
sku 和 mpn 有就写、没有就不写。内部没有独立的 mpn 体系时两个字段填同一个值是常见做法,但不要编一个看起来像行业编码的字符串。
页面真的印着价格区间时
有些询价页会写「参考价 800–1200 元/件,批量另议」。这种情况可以用 AggregateOffer:
"offers": {
"@type": "AggregateOffer",
"priceCurrency": "CNY",
"lowPrice": "800",
"highPrice": "1200",
"offerCount": "6",
"url": "https://example.com/products/px-300"
}
offerCount 填页面上实际能选的配置数量,页面上没有这个数字就别写。B2B 报价含税不含税讲不清时,加 priceSpecification 里的 valueAddedTaxIncluded,别让解析方猜。
只有其中一个型号标了价、其他型号询价,不要用一个 AggregateOffer 把两种状态揉在一起,拆成两个 URL 两个 Product 更干净。同一产品多型号的另一种写法是 ProductGroup 加 hasVariant、variesBy,能表达变体关系,代价是每上新型号都要动标记。
WordPress 里怎么输出
下面这段放进主题的 functions.php 或一个小插件里。假设产品是自定义文章类型 product,规格存在 meta specs 里,一行一条,用 | 分隔名称和值:
add_action( 'wp_head', function () {
if ( ! is_singular( 'product' ) ) {
return;
}
$post_id = get_queried_object_id();
$data = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'@id' => get_permalink( $post_id ) . '#product',
'name' => wp_strip_all_tags( get_the_title( $post_id ) ),
'url' => get_permalink( $post_id ),
];
$excerpt = get_post_field( 'post_excerpt', $post_id );
if ( $excerpt ) {
$data['description'] = wp_strip_all_tags( $excerpt );
}
$image = get_the_post_thumbnail_url( $post_id, 'full' );
if ( $image ) {
$data['image'] = [ $image ];
}
$sku = get_post_meta( $post_id, 'sku', true );
if ( $sku ) {
$data['sku'] = $sku;
$data['mpn'] = $sku;
}
$specs = (string) get_post_meta( $post_id, 'specs', true );
if ( '' !== $specs ) {
$props = [];
foreach ( preg_split( '/\r\n|\r|\n/', $specs ) as $line ) {
$pair = array_map( 'trim', explode( '|', $line, 2 ) );
if ( 2 !== count( $pair ) || '' === $pair[0] || '' === $pair[1] ) {
continue;
}
$props[] = [
'@type' => 'PropertyValue',
'name' => $pair[0],
'value' => $pair[1],
];
}
if ( $props ) {
$data['additionalProperty'] = $props;
}
}
echo '<script type="application/ld+json">'
. wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG )
. '</script>' . "\n";
}, 20 );
JSON_HEX_TAG 不能省。产品名里出现 </script> 这类字符时,没有它,JSON-LD 会提前闭合,剩下的内容会变成页面正文。wp_json_encode 本身不做这个转义。JSON_UNESCAPED_UNICODE 只是让中文别变成一堆 \u 转义,方便人直接看源码。
这段代码对 offers 不做任何默认填充。以后要支持线上下单,判断条件加在 $data 组装之前,别在模板里留一个 price => 0 的兜底。
钩子优先级 20 是留给其他也挂在 wp_head 上的代码。如果你的主题已经在输出 Product,先关掉其中一个——同一页两份 Product、@id 还不一样,解析方会把它们当成两个产品。
上线前逐项核对
- 用 Schema Markup Validator 看语法(多逗号、类型名拼错),用 Rich Results Test 看实际能拿到哪一类体验;
- 逐字段对回页面:标记里出现的价格、库存、评分、评价数,页面上必须看得到,一个都不能多;
image用绝对 URL,别用相对路径或data:;- 多语言站,每种语言的页面用自己的规范 URL 做
@id和url,不要所有语言共用一个; - 开缓存插件后再看一次源码,确认 JSON-LD 还在
<head>里,没有被合并或延迟输出。
不想在主题里维护这段代码的话,AumViso 也提供结构化数据输出,顺带管 sitemap、meta 和内链:https://aumcreate.com/plugins/aumviso?utm_source=aliyun&utm_medium=content&utm_campaign=aliyun-product-schema-inquiry-no-price