搜索全站
Google SEO 17 分钟阅读

Course、JobPosting、Event 结构化数据:垂直内容的标注机会

课程、招聘、活动这三类内容各自有专属富媒体结果。本文给出 JobPosting 与 Event 的完整字段写法、线上活动的 attendanceMode 处理、Course 列表页要求,以及时间与时区的正确格式。

U
UFO_PF 2024-07-10 · 阅读 17 分钟
Co GOOGLE SEO
阅读提示
本文约 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 校验流程所述一致。

常见踩坑

  1. 职位过期不撤:validThrough 到了之后如果不更新或删除,整批职位会被判定为无效。
  2. baseSalary 少了 unitText:无法判断是月薪还是年薪,字段被忽略。
  3. 远程岗仍填 jobLocation 的实体地址:应与 jobLocationType: TELECOMMUTE 二选一,混用会矛盾。
  4. startDate 写 2025-05-01 09:00:空格分隔不是 ISO 8601,正确写法带 T 与时区。
  5. 取消的活动不更新 eventStatus:用户点进去发现已取消,属于典型的负面体验。
  6. 案例:某培训机构把 200 多门课的列表页标成了 Course,但每门课的详情页却没有对应的 Course 节点,provider 也没有与站点级 Organization 对齐。结果 GSC 的 Courses 报表长期显示”缺少字段 provider”。整改时他们在详情页补齐了完整的 Course 节点并把 provider 指向 #organization,三周后课程开始出现在课程类富媒体结果里。列表与详情必须成对存在。
  7. 把 Course 单课程的 hasCourseInstance 时间写成过去:实例过期同样导致资格失效。

总结

判断标准:凡是带”时间 + 地点 + 状态”的信息,都要写得像机器会立刻执行那样精确——时区、截止时间、事件状态一个都不能含糊。行动清单:JobPosting 补 identifier 与 baseSalary.unitText → Event 按线上/线下分流 → Course 列表页与详情页成对且共用 provider → 用脚本每周扫过期时间字段 → 在对应富媒体报表里验收。垂直内容的富媒体收益比通用内容高得多,值得专门维护一套数据校验。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。