本文约 6,562 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:三项被低估的富媒体类型
在 FAQ、HowTo 陆续失去展示价值之后,很多站长觉得”结构化数据没什么可做了”。但有三类内容的富媒体结果依然稳定且转化价值高:招聘信息(JobPosting)、活动(Event)、课程(Course)。它们的共同特点是——用户搜索意图非常明确、结果页会显示结构化摘要(职位的城市与薪资、活动的时间地点、课程的提供方),而这些摘要恰恰决定了点击与否。做过垂直站点的朋友都知道,一个能显示”上海 · 全职 · 20-30K”的职位结果,点击率比纯文本高出一截。这三类的门槛不在于难,而在于字段多且时间类字段极易写错:缺时区、少了必填项、虚标已过期的职位,都会让整块节点被丢弃。
原理分析:时间、地点、有效期限是三条主线
先说 JobPosting。必填字段:title、description(HTML 富文本,需包含职责与要求)、hiringOrganization、jobLocation(远程岗则用 jobLocationType 为 TELECOMMUTE 并配 applicantLocationRequirements)、datePosted、validThrough、employmentType、identifier、baseSalary。其中 validThrough 必须落在未来,过期职位会被移富集-desktopdiv 除富媒体资格。
再说 Event。必填 name、startDate、location。这里有一个容易忽略的分支:线上活动必须同时声明 eventAttendanceMode 为 OnlineEventAttendanceMode,并把 location 写成 VirtualLocation 且带 url(即直播间地址);线下活动则是 OfflineEventAttendanceMode 配 Place 与 address。时间序列相关的 startDate/endDate 必须带时区偏移。eventStatus 用 EventScheduled/EventCancelled/EventPostponed 表达状态。
最后是 Course。它要求同一列表页上的所有课程来自同一提供方,列表页每个 Course 带 name 与 description,并指向各自的详情页,详情页再标 Course(含 provider)。Google 的 Course 富媒体形态经历过调整,覆盖面有限,因此务必先做 tool 验证,不要默认一定有 UI 效果。
| 类型 | 最易出错的字段 | 后果 |
|---|---|---|
| JobPosting | validThrough 过期 | 职位被移出结果 |
| JobPosting | description 过于单薄 | 不被判为有效职位页 |
| Event | startDate 无时区 | 时间解析失败 |
| Event | 线上活动仍用 Place | 地点矛盾导致忽略 |
| Course | 列表与详情页不一致 | 课程被跳过 |
保姆级实操
第一步:招聘页补齐 JobPosting 必填字段
{
"@context": "https://schema.org",
"@type": "JobPosting",
"title": "WordPress 高级开发工程师",
"description": "<p>负责主题与插件开发、性能优化。</p><ul><li>要求 3 年以上 PHP 经验</li><li>熟悉 MySQL 与 Redis</li></ul>",
"identifier": {
"@type": "PropertyValue",
"name": "迷芒飞",
"value": "ENG-2025-018"
},
"datePosted": "2025-04-02T09:00:00+08:00",
"validThrough": "2025-06-30T23:59:59+08:00",
"employmentType": "FULL_TIME",
"hiringOrganization": {
"@type": "Organization",
"name": "迷芒飞科技",
"sameAs": "https://mimangfei.com/",
"logo": "https://mimangfei.com/wp-content/uploads/logo.png"
},
"jobLocationType": "TELECOMMUTE",
"applicantLocationRequirements": {
"@type": "Country",
"name": "CN"
},
"baseSalary": {
"@type": "MonetaryAmount",
"currency": "CNY",
"value": {
"@type": "QuantitativeValue",
"minValue": 25000,
"maxValue": 40000,
"unitText": "MONTH"
}
}
}
第二步:活动页区分线上与线下
add_action( 'wp_head', function () {
if ( ! is_singular( 'event' ) ) {
return;
}
$post_id = get_the_ID();
$start = get_post_meta( $post_id, '_event_start', true ); // 存 Y-m-d H:i
if ( ! $start ) {
return;
}
$ts = strtotime( $start . ' ' . wp_timezone_string() );
$start_iso = gmdate( 'c', $ts );
$online = (bool) get_post_meta( $post_id, '_event_online', true );
$node = [
'@context' => 'https://schema.org',
'@type' => 'Event',
'name' => get_the_title( $post_id ),
'startDate' => $start_iso,
'eventAttendanceMode' => $online
? 'https://schema.org/OnlineEventAttendanceMode'
: 'https://schema.org/OfflineEventAttendanceMode',
'location' => $online ? [
'@type' => 'VirtualLocation',
'url' => get_post_meta( $post_id, '_event_join_url', true ),
] : [
'@type' => 'Place',
'name' => get_post_meta( $post_id, '_event_venue', true ),
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => get_post_meta( $post_id, '_event_street', true ),
'addressLocality' => get_post_meta( $post_id, '_event_city', true ),
'addressCountry' => get_post_meta( $post_id, '_event_country', true ),
],
],
'eventStatus' => 'https://schema.org/EventScheduled',
];
echo '<script type="application/ld+json">' . wp_json_encode( $node, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) . '</script>';
}, 20 );
第三步:课程列表页与详情页分工
列表页输出多个 Course 摘要节点,provider 必须与Organization / WebSite 站点级结构化数据里声明的组织实体一致,最好直接用 @id 引用。
{
"@context": "https://schema.org",
"@type": "ItemList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"item": {
"@type": "Course",
"name": "WordPress 性能优化实战",
"description": "覆盖缓存、数据库、图片、前端渲染四个方向的性能优化。",
"provider": { "@id": "https://mimangfei.com/#organization" },
"url": "https://mimangfei.com/courses/wp-performance/"
}
}
]
}
第四步:批量体检所有垂直内容页
把 JobPosting 的过期职位批量找出来,是最容易做成脚本的体检任务:
today=$(date -u +%Y-%m-%d)
for u in $(curl -s https://mimangfei.com/wp-sitemap-posts-job-1.xml | grep -o '<loc>[^<]*</loc>' | sed 's/<[^>]*>//g'); do
vt=$(curl -sL "$u" | grep -o '"validThrough": *"[^"]*"' | head -1 | cut -d'"' -f4)
echo "$today $vt $u"
done | awk '$2 < $1 {print "EXPIRED:", $0} END {print "scan done"}'
第五步:在 Rich Results Test 里逐个验证并盯紧 GSC 报表
三类都有各自的富媒体报表(Jobs、Events、Courses),修饰修复流程与Rich Results Test 与 Schema Markup Validator 校验流程所述一致。
常见踩坑
- 职位过期不撤:
validThrough到了之后如果不更新或删除,整批职位会被判定为无效。 baseSalary少了unitText:无法判断是月薪还是年薪,字段被忽略。- 远程岗仍填
jobLocation的实体地址:应与jobLocationType: TELECOMMUTE二选一,混用会矛盾。 startDate写2025-05-01 09:00:空格分隔不是 ISO 8601,正确写法带T与时区。- 取消的活动不更新
eventStatus:用户点进去发现已取消,属于典型的负面体验。 - 案例:某培训机构把 200 多门课的列表页标成了 Course,但每门课的详情页却没有对应的 Course 节点,
provider也没有与站点级 Organization 对齐。结果 GSC 的 Courses 报表长期显示”缺少字段 provider”。整改时他们在详情页补齐了完整的 Course 节点并把 provider 指向#organization,三周后课程开始出现在课程类富媒体结果里。列表与详情必须成对存在。 - 把 Course 单课程的
hasCourseInstance时间写成过去:实例过期同样导致资格失效。
总结
判断标准:凡是带”时间 + 地点 + 状态”的信息,都要写得像机器会立刻执行那样精确——时区、截止时间、事件状态一个都不能含糊。行动清单:JobPosting 补 identifier 与 baseSalary.unitText → Event 按线上/线下分流 → Course 列表页与详情页成对且共用 provider → 用脚本每周扫过期时间字段 → 在对应富媒体报表里验收。垂直内容的富媒体收益比通用内容高得多,值得专门维护一套数据校验。
