a-blog cms × 構造化データ

みなさん、サイトのリッチリザルト対応はしていますでしょうか?
対応することで、Googleの検索結果にリッチな表現で表示できます。これにより、他の検索結果ページよりも目を引くような表示でユーザーの目を引き、アクセスしやすいサイトになり、間接的なSEO向上が見込めるというメリットがあります。

動的コンテンツになると特に実装が難しそうというイメージもありますが、a-blog cms ならモジュールとカスタムフィールドを組み合わせていけば基本的にはHTMLのマークアップのみで実装可能です。
※リッチリザルトについての詳細は、Google 検索がサポートする構造化データ マークアップ | Google 検索セントラル  |  ドキュメント  |  Google for Developers でご確認いただけます。

公式テーマに入っている構造化データの紹介

a-blog cms の公式標準テーマ(siteテーマなど)には、あらかじめパンクズリスト構造化データが実装されています。この実装により、Googleの検索エンジンは対象ページの階層表示を検索結果に表示できます。


パンクズリスト表示例


階層表示の出力には、Topicpathモジュールを使用しています。

【例)themes/site/include/head/structured-data.html(a-blog cms Ver.3.1.3 の場合)】

<!-- BEGIN_MODULE Topicpath id="モジュールID" -->
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
  <!-- BEGIN blog:loop --><!-- BEGIN glue -->,<!-- END glue -->{
    "@type": "ListItem",
    "position": {sNum},
    "item":
    {
    "@id": "{url}",
    "name": "{name}"
    }
  }<!-- END blog:loop --><!-- BEGIN category:loop --><!-- BEGIN glue -->,<!-- END glue -->
  {
    〜 割愛(blog:loopの中と同様の記述) 〜
  }<!-- END category:loop --><!-- BEGIN entry --><!-- BEGIN glue -->,<!-- END glue -->
  {
    〜 割愛(blog:loopの中と同様の記述) 〜
  }<!-- END entry -->
  ]
}
</script>
<!-- END_MODULE Topicpath -->

Topicpathモジュールにあらかじめ用意されている変数のみを使用しており、特別なカスタマイズは行っていません。

このように、パンクズリストに限らず、a-blog cms で標準実装されているモジュールを使用して、ほとんどの構造化データを実装できます。

記事の構造化データ実装方法紹介

比較的簡単に実装可能な記事の構造化データによるリッチリザルト対応方法を紹介します。

ニュース、ブログ、スポーツの記事のページに Article 構造化データを追加すると、Google がウェブページの詳細を理解し、Google 検索やその他のサービス(Google ニュースや Google アシスタントなど)の検索結果で、その記事のタイトル テキスト、画像、日付情報を適切に表示できるようになります。

記事(Article)の構造化データ | Google 検索セントラル | ドキュメント | Google for Developers

上記のように、お知らせやブログのエントリー詳細ページに追加できるのが記事の構造化データです。これを実装することで、Google検索や関連サービスの検索結果でサムネイルが表示されるようになります。


NewsArticle例(2023/10現在検索結果)


記事の構造化データの項目は大きく6つです。
全ての項目が「推奨」項目となります。



@type ニュース記事の場合は NewsArticle、ブログ記事の場合は BlogPosting、それ以外または特定のカテゴリーではないような記事の場合は Article を指定
headline 記事タイトル
image 画像URLを指定
datePublished 記事公開日
dateModified 記事更新日
author 記事の作成者情報(タイプ、名前、著者の情報が載っているURL)を指定
author についての注意点
タイプ(author.@type)とURL(author.url)について、推奨項目となっていますが、Googleのガイドラインにて「使用することを強くおすすめします。」と記載されているので、できれば指定することが望ましいようです。※Googleのガイドライン > 記事(Article)の構造化データ 作成者のマークアップのベストプラクティス
image についての注意点
推奨項目となっていますが、Googleのガイドラインにて「すべてのページに画像を少なくとも 1 つ含める必要があります」と記載されているので、できれば画像を挿入するようにすることが望ましいようです。※Googleのガイドライン > 記事(Article)の構造化データタイプの定義

※記事の構造化データの詳細については、記事(Article)の構造化データ | Google 検索セントラル | ドキュメント | Google for Developers でご確認いただけます。

記事の構造化データ実装例

記事詳細は基本的に Entry_Bodyモジュール を使用していると思いますので、構造化データも Entry_Bodyモジュールの変数を使用して作成していきます。

【 _entry.html 】

@extends("/_layouts/継承元ファイル名.html")

@section(head-js)
@parent
<!-- GET_Rendered id="ld_json_article_entry" -->
@endsection

@section("main")
@include("/include/entry/body.html")
@endsection
【 include/entry/body.html 】

<!-- BEGIN_MODULE Entry_Body id="モジュールID" -->
<!-- BEGIN notFound -->〜割愛〜<!-- END notFound -->
<!-- BEGIN entry:loop -->
〜記事内容のページ出力割愛〜

<!-- BEGIN_SetRendered id="ld_json_article_entry" --><script type="application/ld+json">
{
  "headline": "{title}",<!-- BEGIN_MODULE Ogp -->
  "image": [
    "%{HTTP_MEDIA_ARCHIVES_DIR}{image}[resizeImgFit(304,171)]",
    "%{HTTP_MEDIA_ARCHIVES_DIR}{image}[resizeImgFit(260,195)]",
    "%{HTTP_MEDIA_ARCHIVES_DIR}{image}[resizeImgFit(224,224)]"
  ],<!-- END_MODULE Ogp --><!-- BEGIN date:veil -->
  "datePublished": "{date#Y}-{date#m}-{date#d}T{date#H}:{date#i}:{date#s}+09:00",<!-- END date:veil --><!-- BEGIN_IF [{date#Y}{date#m}{date#d}/neq/{udate#Y}{udate#m}{udate#d}] --><!-- BEGIN udate:veil -->
  "dateModified": "{udate#Y}-{udate#m}-{udate#d}T{udate#H}:{udate#i}:{udate#s}+09:00",<!-- END udate:veil --><!-- END_IF --><!-- BEGIN userField:veil --><!-- BEGIN userField -->
  "author": [{
    "name": "{fieldUserName}",<!-- BEGIN fieldUserUrl -->
    "url": "{fieldUserUrl}",<!-- END fieldUserUrl -->
    "@type": "Person"
  }],<!-- END userField --><!-- END userField:veil -->
  "@type": "NewsArticle",
  "@context": "https://schema.org"
}
</script><!-- END_SetRendered -->

<!-- END entry:loop -->
〜ページャー割愛〜
<!-- END_MODULE Entry_Body -->

※複数の著者(author)を設定する場合は、カスタムフィールドグループで著者の入力欄を実装する必要があります。

※SetRendered を使用して entry:loop 内で出力した JSON を headタグ内で読み込んでいます。SetRendered についての詳細は テンプレートの変数化 | テンプレート | ドキュメント | a-blog cms developer でご確認いただけます。

※画像は校正オプションでアスペクト比 16x9、4x3、1x1 のサイズを生成しています。設定値は解像度が 50,000px 以上となるようにしています。メイン画像を読み込むのにOGPモジュールを使用しています。

イベント構造化データ実装方法紹介

2023年10月現在、イベントの開催地がオンラインで行われる機会が増えたことから、構造化データのオプションも増加し、実装がより複雑になりました。イベントに関しては、複数の専用カスタムフィールドを用意して実装する必要があるため、「難しめの構造化データ」の実装方法として紹介します。

Google のイベント機能を活用すると、ユーザーは Google 検索結果や他の Google サービス(Google マップなど)からイベントを見つけ、参加しやすくなります。

イベント(Event)の構造化データ | Google 検索セントラル | ドキュメント | Google for Developers

上記のように、イベント構造化データを実装すると Googleサービスで強調表示してくれるため集客などにも繋げやすくなるのではないでしょうか。

イベント構造化データの項目は大きくわけると11項目です。



@type 必須 Event
name 必須 イベント名
eventStatus 推奨 5つのステータス(開催予定や中止など)の中から指定
※未設定の場合は EventScheduled(開催予定)としてみなされる
eventAttendanceMode 推奨 オフラインかオンラインか両方かを選択
※未設定の場合は OfflineEventAttendanceMode(オフライン)としてみなされる
startDate 必須 開始日時
endDate 推奨 終了日時
description 推奨
概要
※イベント内容について記載、日時や場所は専用項目で指定する
image 推奨
イベントの画像やロゴ
location 必須 イベント開催場所
performer 推奨 出演者の情報(グループか個人を指定、名前)
organizer 推奨 主催者の情報(組織か個人を指定、名前、主催者のURL)
offers 推奨 チケット情報(販売中などのステータス、価格情報、など...)
previousStartDate 推奨 イベントの日程が変更された場合、変更前に予定されていたイベント開始日を設定
startDate, endDate についての注意点
時間がわからない場合は時間指定しないでください。オンラインイベントの場合は開催時間の最後に日本の場合(JST)のUTC/GMTタイムオフセット「+09:00」が必須となります。

※イベント構造化データの詳細は、イベント(Event)の構造化データ | Google 検索セントラル  |  ドキュメント  |  Google for Developers でご確認いただけます。

イベント構造化データ実装例

1イベント1エントリーで作成した場合を想定し、実装例を出しました。使用するモジュールは Entry_Bodyモジュール です。
イベントに関しては、Entry_Bodyモジュールの標準変数だけでは情報不足なため、いくつかのカスタムフィールドを用意する必要があります。

エントリー編集ページへのカスタムフィールド追加

編集ページ側にエントリーのカスタムフィールドを追加します。
例)/admin/entry/field.html

イベント基本情報
<table class="acms-admin-table-admin-edit acms-admin-margin-bottom-mini">
  <tr>
    <th style="width: 130px;">イベント状況</th>
    <td>
      <select name="event_status">
        <option value="EventScheduled" {event_status:selected#EventScheduled}>開催</option>
        <option value="EventCancelled" {event_status:selected#EventCancelled}>中止</option>
        <option value="EventMovedOnline" {event_status:selected#EventMovedOnline}>オフラインからオンラインに変更</option>
        <option value="EventPostponed" {event_status:selected#EventPostponed}>延期だが日程未定</option>
      </select>
      <input type="hidden" name="field[]" value="event_status">
    </td>
  </tr>
  <tr>
    <th style="width: 130px;">開催日時<i class="acms-admin-icon-tooltip js-acms-tooltip" data-acms-tooltip="秒数は自動的に開始:00、終了:59として出力します"></i></th>
    <td>
      <table class="acms-admin-table-admin-edit">
        <tr>
          <th style="width: 110px;">設定</th>
          <td>
            <select name="event_time_status">
              <option value="" {event_time_status:selected#}>---</option>
              <option value="start_time_undecided" {event_time_status:selected#start_time_undecided}>イベント開始時間未定</option>
              <option value="all_day" {event_time_status:selected#all_day}>終日イベント</option>
            </select>
            <input type="hidden" name="field[]" value="event_time_status" />
          </td>
        </tr>
        <tr>
          <th style="width: 110px;">開始日時</th>
          <td>
            <p class="acms-admin-margin-none">※エントリー日付で設定</p>
          </td>
        </tr>
        <tr>
          <th style="width: 110px;">終了日時<i class="acms-admin-icon-tooltip js-acms-tooltip" data-acms-tooltip="終日イベントの場合は入力不要"></i></th>
          <td>
            <input type="text" name="event_end_date" value="{event_end_date}" class="acms-admin-form-width-mini acms-admin-margin-right-mini js-datepicker2" placeholder="YYYY-MM-DD">
            <input type="text" name="event_end_time" value="{event_end_time}" class="acms-admin-form-width-mini js-timepicker" placeholder="HH:MM:SS">
            <input type="hidden" name="field[]" value="event_end_date">
            <input type="hidden" name="field[]" value="event_end_time">
          </td>
        </tr>
      </table>
    </td>
  </tr>
  <tr>
    <th style="width: 130px;">開催場所 <span class="acms-admin-label acms-admin-label-danger">必須</span></th>
    <td>
      <select name="event_attendance_mode">
        <option value="" {event_attendance_mode:selected#}>未定</option>
        <option value="OfflineEventAttendanceMode" {event_attendance_mode:selected#OfflineEventAttendanceMode}>オフライン</option>
        <option value="OnlineEventAttendanceMode" {event_attendance_mode:selected#OnlineEventAttendanceMode}>オンライン</option>
        <option value="MixedEventAttendanceMode" {event_attendance_mode:selected#MixedEventAttendanceMode}>オフラインとオンラインの両方</option>
      </select>
      <input type="hidden" name="field[]" value="event_attendance_mode">
    </td>
  </tr>
  <tr>
    <th style="width: 130px;">オフライン</th>
    <td>
      <table class="acms-admin-table-admin-edit">
        <tr>
          <th style="width: 110px;">会場名</th>
          <td>
            <input type="text" name="event_location_name" value="{event_location_name}" class="acms-admin-form-width-full">
            <input type="hidden" name="field[]" value="event_location_name">
          </td>
        </tr>
        <tr>
          <th style="width: 110px;">会場住所<i class="acms-admin-icon-tooltip js-acms-tooltip" data-acms-tooltip="開催場所がオフラインの場合必須"></i></th>
          <td>
            <input type="text" name="event_location_address" value="{event_location_address}" class="acms-admin-form-width-full" placeholder="〇〇県〇〇市〜">
            <input type="hidden" name="field[]" value="event_location_address">
          </td>
        </tr>
      </table>
    </td>
  </tr>
  <tr>
    <th style="width: 130px;">オンライン</th>
    <td>
      <table class="acms-admin-table-admin-edit">
        <tr>
          <th style="width: 110px;">開催URL<i class="acms-admin-icon-tooltip js-acms-tooltip" data-acms-tooltip="開催場所がオンラインの場合必須"></i></th>
          <td>
            <input type="text" name="event_location_offline_url" value="{event_location_offline_url}" class="acms-admin-form-width-full" placeholder="https://~">
            <input type="hidden" name="field[]" value="event_location_offline_url">
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>
イベント詳細情報
<table class="acms-admin-table-admin-edit acms-admin-margin-bottom-mini">
  <tr>
    <th style="width: 130px;">イベント概要</th>
    <td>
      <textarea name="event_description" class="acms-admin-form-width-full" rows="5">{event_description}</textarea>
      <input type="hidden" name="field[]" value="event_description">
    </td>
  </tr>
  <tr>
    <th style="width: 130px;">メイン画像</th>
    <td class="js-media-field">
      <div class="js-droparea" data-thumbnail="{main_image@thumbnail}" data-type="image" data-thumbnail-type="{main_image@type}" data-width="200px" data-height="200px"></div>
      <p class="js-text acms-admin-text-danger" style="display:none">許可されていないファイルのため挿入できません。</p>
      <div class="acms-admin-margin-top-mini">
        <button type="button" class="js-insert acms-admin-btn" data-type="image">メディアを選択</button>
      </div>
      <input type="hidden" name="main_image" value="{main_image}" class="js-value">
      <input type="hidden" name="field[]" value="main_image">
      <input type="hidden" name="main_image:extension" value="media">
      <p class="acms-admin-margin-none acms-admin-margin-top-mini">※推奨サイズ:幅720px以上</p>
    </td>
  </tr>
</table>
performer, organizer, offers(出演者情報、主催者情報、チケット情報)について
推奨プロパティなので上記実装例からは除外しました。実際に実装する際は、イベントページに合わせて項目追加してみてください。
previousStartDate(開催日変更をした場合の、変更前の開始日)について
イベントの日程が変更された場合、変更前に予定されていたイベント開始日を設定できる previousStartDate という項目もありますが、実装するとより複雑化するため推奨プロパティということもあり、上記実装例からは除外しました。それに伴い eventStatus からは EventRescheduled(イベント日時を変更した)の選択肢を無くしています。

エントリー詳細ページ

【 _entry.html 】

@extends("/_layouts/継承元ファイル名.html")

@section(head-js)
@parent
<!-- GET_Rendered id="ld_json_event_entry" -->
@endsection

@section("main")
@include("/include/entry/body.html")
@endsection
【 include/entry/body.html 】

<!-- BEGIN_MODULE Entry_Body id="モジュールID" -->
<!-- BEGIN notFound -->〜割愛〜<!-- END notFound -->
<!-- BEGIN entry:loop -->
〜イベント内容のページ出力割愛〜

<!-- BEGIN_SetRendered id="ld_json_event_entry" --><!-- BEGIN_IF [{event_attendance_mode}/nem] --><script type="application/ld+json">
{
  "name": "{title}",<!-- BEGIN_IF [{event_status}/nem] -->
  "eventStatus": "https://schema.org/{event_status}",<!-- END_IF -->
  "eventAttendanceMode": "https://schema.org/{event_attendance_mode}",<!-- BEGIN_IF [{event_time_status}/eq/all_day] -->
  "startDate": "{date#Y}-{date#m}-{date#d}",
  "endDate": "{date#Y}-{date#m}-{date#d}",<!-- ELSE_IF [{event_time_status}/eq/start_time_undecided] -->
  "startDate": "{date#Y}-{date#m}-{date#d}",<!-- BEGIN event_end_date:veil -->
  "endDate": "{event_end_date}",<!-- END event_end_date:veil --><!-- ELSE -->
  "startDate": "{date#Y}-{date#m}-{date#d}T{date#H}:{date#i}:00+09:00",<!-- BEGIN_IF [{event_end_date}/nem] -->
  "endDate": "{event_end_date}<!-- BEGIN event_end_time:veil -->T{event_end_time}[trim(5, '')]:59+09:00<!-- END event_end_time:veil -->",<!-- END_IF --><!-- END_IF --><!-- BEGIN event_description:veil -->
  "description": "{event_description}[delnl]",<!-- END event_description:veil --><!-- BEGIN_IF [{main_image@path}/nem] -->
  "image": [
    "%{HTTP_MEDIA_ARCHIVES_DIR}{main_image@path}[resizeImgFit(720,720)]",
    "%{HTTP_MEDIA_ARCHIVES_DIR}{main_image@path}[resizeImgFit(720,540)]",
    "%{HTTP_MEDIA_ARCHIVES_DIR}{main_image@path}[resizeImgFit(720,405)]"
  ],<!-- END_IF --><!-- BEGIN_IF [{event_attendance_mode}/eq/OfflineEventAttendanceMode] -->
  "location": {
    "@type": "Place",<!-- BEGIN event_location_name:veil -->
    "name": "{event_location_name}",<!-- END event_location_name:veil -->
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "{event_location_address}",
      "addressCountry": "日本"
    }
  },<!-- ELSE_IF [{event_attendance_mode}/eq/OnlineEventAttendanceMode] -->
  "location": {
    "@type": "VirtualLocation",
    "url": "{event_location_offline_url}"
  },<!-- ELSE -->
  "location": [{
    "@type": "VirtualLocation",
    "url": "{event_location_offline_url}"
  },
  {
    "@type": "Place",<!-- BEGIN event_location_name:veil -->
    "name": "{event_location_name}",<!-- END event_location_name:veil -->
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "{event_location_address}",
      "addressCountry": "日本"
    }
  }],<!-- END_IF -->
  "@type": "Event",
  "@context": "https://schema.org"
}
<!-- END_IF --></script><!-- END_SetRendered -->
<!-- END entry:loop -->
〜ページャー割愛〜
<!-- END_MODULE Entry_Body -->

※SetRendered を使用して entry:loop 内で出力した JSON を headタグ内で読み込んでいます。SetRendered についての詳細は テンプレートの変数化 | テンプレート | ドキュメント | a-blog cms developer でご確認いただけます。

※画像は校正オプションでアスペクト比 16x9、4x3、1x1 のサイズを生成しています。画像の規定最小幅は720pxです。

最後に

a-blog cms での構造化データ実装は、モジュールとカスタムフィールドの組み合わせで基本的にはどの構造化データの実装も可能です。まだチャレンジしたことがない方は以下にピックアップした実装時のコツを押さえつつ実装してみてください!

実装時のコツ

同じタグ付けがされている記事