WordPressにJSON-LDでパンくずを設定:メモ

悩み
悩み

いまいちイメージが掴めていなかったJSON-LDでパンくずの設定を試したのでメモ。

なるべく共通部分をまとめて分岐させてと考えていましたが、現時点ではあまりまとめず切り分けやすい形に落ち着きました。

もう少しきれいにできればいいなと思いますが、今はこの辺で。

前提

以下前提です。

  • JSON-LD部分だけを別ファイルにまとめる(仮にbreadcrumb-json.phpとする)
  • breadcrumb-json.phpをフッターで読み込む
  • TOPページには読み込ませない

TOPページに読み込ませないのは、必要が無いファイルの必要の無い分岐を通すことが動作を遅くするのではと考えたためです。

そこで、最初にTOPページを除外してからファイルを読み込ませる流れで検討しました。

JSON-LDのパンくず:方法1

以下をbreadcrumb-json.phpとして作成します。


<?php if(is_single()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
//パンくずの階層用
$i=1;
//カテゴリーに関する情報を取得
$categories = get_the_category($post->ID);
$cat = $categories[0];
//先祖のカテゴリーがあれば(0でなければ)分岐
if($cat -> parent != 0){
    //先祖のカテゴリーを配列で取得
    $ancestors = array_reverse(get_ancestors( $cat -> cat_ID, 'category' ));
    //$ancestorsの配列から一つ一つ$ancestorに取り出してなくなるまでくりかえす
    foreach($ancestors as $ancestor){
        $i++;
        echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. get_category_link($ancestor).'","name": "'. get_cat_name($ancestor). '"}},'.PHP_EOL;
    }
}
//属していてる直接のカテゴリーの情報を出力
$i++;
echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. get_category_link($cat -> term_id). '","name": "'. $cat-> cat_name . '"}},'.PHP_EOL;
//表示されている投稿ページの情報を出力
$i++;
echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. esc_url(get_permalink()). '","name": "'. esc_html(get_the_title()) . '"}}'.PHP_EOL;
?>
  ]
}
</script>

<?php elseif(is_page()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
//ベージごとに必要な情報のベースを取得。先祖の有無判断に利用。
$obj = get_queried_object();
$i=1;
//先祖の固定ページがあれば(0でなければ)分岐
if($obj -> post_parent != 0){
    //先祖の固定ページを配列で取得
    $pageAncestors = array_reverse( $post -> ancestors );
    //$ancestorsの配列から一つ一つ$ancestorに取り出してなくなるまでくりかえす
    foreach ($pageAncestors as $pageAncestor) {
        $i++;
        echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. esc_url(get_permalink($pageAncestor)).'","name": "'. esc_html(get_the_title($pageAncestor)). '"}},'.PHP_EOL;
    }
}
//表示されている固定ページの情報を出力
$i++;
echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. esc_url(get_permalink()). '","name": "'. esc_html(get_the_title()) . '"}}'.PHP_EOL;
?>
  ]
}
</script>

<?php elseif(is_category()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
//パンくずの階層用
$i=1;
//カテゴリーに関する情報を取得
$categories = get_the_category($post->ID);
$cat = $categories[0];
//カテゴリーアーカイブのタイトルを取得
$cattitle = get_the_archive_title();
//先祖のカテゴリーがあれば(0でなければ)分岐
if($cat -> parent != 0){
    //先祖のカテゴリーを配列で取得
    $ancestors = array_reverse(get_ancestors( $cat -> cat_ID, 'category' ));
    //$ancestorsの配列から一つ一つ$ancestorに取り出してなくなるまでくりかえす
    foreach($ancestors as $ancestor){
        $i++;
        echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. get_category_link($ancestor).'","name": "'. get_cat_name($ancestor). '"}},'.PHP_EOL;
    }
}
//表示されているカテゴリーの情報を出力
$i++;
echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. get_category_link($cat -> term_id). '","name": "'. $cattitle . '"}}'.PHP_EOL;
?>
  ]
}
</script>

<?php elseif(is_tag()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
$tagName = single_tag_title('', false);
$tag = get_term_by('name', $tagName, 'post_tag');
$link = get_tag_link($tag->term_id);
echo '    {"@type": "ListItem","position": 2,"item":{"@id": "'. esc_url($link). '","name": "'. esc_html($tagName) . '"}}'.PHP_EOL;
?>
  ]
}
</script>

<?php elseif(is_author()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
//執筆者のIDを取得
$userId = get_query_var('author');
//執筆者の名前を取得
$authorName = get_the_author_meta( 'display_name', $userId );
echo '    {"@type": "ListItem","position": 2,"item":{"@id": "'. esc_url(get_author_posts_url($userId)). '","name": "'. esc_html($authorName) . '"}}'.PHP_EOL;
?>
  ]
}
</script>

<?php elseif(is_date()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
//年月日を取得
$y = get_query_var('year');
$m = get_query_var('monthnum');
$d = get_query_var('day');
//年月日のアーカイブのリンクを取得
$linkY = get_year_link($y);
$linkM = get_month_link($y,$m);
$linkD = get_month_link($y,$m,$d);
if(is_day()){
    echo '    {"@type": "ListItem","position": 2,"item":{"@id": "'. esc_url($linkY).'","name": "'. esc_html($y).'年"}},'.PHP_EOL;
    echo '    {"@type": "ListItem","position": 3,"item":{"@id": "'. esc_url($linkM).'","name": "'. esc_html($m).'月"}},'.PHP_EOL;
    echo '    {"@type": "ListItem","position": 4,"item":{"@id": "'. esc_url($linkD).'","name": "'. esc_html($d). '日"}}'.PHP_EOL;
} elseif(is_month()){
    echo '    {"@type": "ListItem","position": 2,"item":{"@id": "'. esc_url($linkY).'","name": "'. esc_html($y).'年"}},'.PHP_EOL;
    echo '    {"@type": "ListItem","position": 3,"item":{"@id": "'. esc_url($linkM).'","name": "'. esc_html($m).'月"}}'.PHP_EOL;
} elseif(is_year()) {
    echo '    {"@type": "ListItem","position": 2,"item":{"@id": "'. esc_url($linkY).'","name": "'. esc_html($y).'年"}}'.PHP_EOL;
}
?>
  ]
}
</script>

<?php elseif(is_search()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
echo '    {"@type": "ListItem","position": 2,"item":{"@id": "'. esc_url(get_search_link()). '","name": "「'. esc_html(get_search_query()) . '」で検索した結果"}}'.PHP_EOL;
?>
  ]
}
</script>

<?php elseif(is_attachment()): ?>
<script type="application/ld+json">
{ "@context":"http://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement":
  [
    {"@type": "ListItem","position": 1,"item":{"@id": "<?php echo home_url(); ?>","name": "ホーム"}},
<?php
//パンくずの階層用
$i=1;
//ベージごとに必要な情報のベースを取得。先祖の有無判断に利用。
$obj = get_queried_object();
//先祖の挿入元のページがあれば(0でなければ)分岐
if($obj -> parent != 0){
    $i++;
    echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. esc_url(get_permalink($pageAncestor)).'","name": "'. esc_html(get_the_title($pageAncestor)). '"}},'.PHP_EOL;
}
//表示されている固定ページの情報を出力
$i++;
echo '    {"@type": "ListItem","position": '.$i.',"item":{"@id": "'. esc_url(get_permalink()). '","name": "'. esc_html(get_the_title()) . '"}}'.PHP_EOL;
?>
  ]
}
</script>
<?php endif; ?>

いろいろ悩んだのですが、ifで分けた部分ごとに完結している形にしました。

無駄が多いのですが、多分必要とされないだろうなと思われる以下のページ分も作成しているため、それらを後で簡単に増減できるようにと考えたためです。

  • 日別アーカイブ
  • 著者
  • 添付

上記のように記述したbreadcrumb-json.phpを以下の用にfooter.phpで読み込みます。


<?php if(!is_front_page()): ?>
//テーマ内のfooter.phpと同階層にbreadcrumb-json.phpがあると想定
<?php get_template_part('breadcrumb-json) ;?>
<?php endif; ?>

わざわざこの形を検討した理由は冒頭で述べた通りです。

ここを気にするならもっと他にやるべきことがあるのではとも思いますが、やはり一応試してみたいなと。

JSON-LDのパンくず:方法2

json_encodeを利用した方法ですが、ほとんど利用経験がないので方法1よりも力技だと思います。


<?php
//404なら中止
if(is_404()){ return; }

/**
 * schema.orgをJSON-LDで書き出す
 */
function my_breadcrumb_json_set (array $args) {
  echo '<script type="application/ld+json">' , PHP_EOL;
  echo json_encode( $args, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ) , PHP_EOL;
  echo '</script>' , PHP_EOL;
}

/**
 * 最上位のホームの設定
 * @param array $homeArgs
 */
function my_breadcrumb_json_home(){
  $homeUrl = esc_url( get_home_url('/') );
  $homeArgs[]  = array(
    "@type" => "ListItem",
    "position" => 1,
    "item" => array(
      "@id" => $homeUrl,
      "name" => "ホーム"
    )
  );
  return $homeArgs;
}

/**
 * 現在表示されているページのタイトルとURLを取得。
 * 日別アーカイブはすべて分岐で作成するため不要。
 * @param array $currentArgs
 */
function my_breadcrumb_json_current_page($i){
  global $post;
  if(is_category()){
    $cat = get_queried_object();
    $currentUrl  = get_category_link($cat -> term_id);
    $currentTitle = single_cat_title(", false);
  } elseif(is_tag()) {
    $currentTitle = single_tag_title('', false);
    $tag = get_term_by('name', $currentTitle, 'post_tag');
    $currentUrl= get_tag_link($tag ->term_id);
    //祖先の固定ページがあれば(0でなければ)分岐
  } elseif(is_author()) {
    //執筆者のIDを取得
    $userId = get_query_var('author');
    //執筆者の名前を取得
    $currentTitle = get_the_author_meta( 'display_name', $userId );
    $currentUrl = get_author_posts_url($userId);
    //祖先の固定ページがあれば(0でなければ)分岐
  } elseif(is_search()) {
    $currentTitle = "「".get_search_query()."」の検索結果";
    $currentUrl = get_search_link();
    //祖先の固定ページがあれば(0でなければ)分岐
  } else {
    $currentUrl = get_permalink();
    $currentTitle = get_the_title();
  }
  $currentArgs[] = array(
    "@type" => "ListItem",
    "position" => $i,
    "item" => array(
      "@id" => esc_url($currentUrl),
      "name" => esc_html($currentTitle)
    )
  );
  return $currentArgs;
}

/**
 * ループを用いて $individualArgs に祖先の情報を代入
 * @param array $individualArgs
 */
if(is_attachment()){ //添付ファイル
  //ベージごとに必要な情報のベースを取得。祖先の有無判断に利用。
  $obj = get_queried_object();
  //パンくずの階層用
  $i=1;
  //祖先の固定ページがあれば(0でなければ)分岐
  if($obj -> parent != 0){
    $i++;
    $pageAncestors = array_reverse( $post -> ancestors );
    $pageName = get_the_title($pageAncestor);
    $pageUrl = get_permalink($pageAncestor);
    $individualArgs[] = array(
      "@type" => "ListItem",
      "position" => $i,
      "item" => array(
        "@id" => esc_url($pageUrl) ,
        "name" => esc_html($pageName)
      )
    );
  }
} elseif(is_single()){ //投稿ページ
  //パンくずの階層用
  $i=1;
  //カテゴリーに関する情報を取得
  $categories = get_the_category($post->ID);
  $cat = $categories[0];
  if($cat -> parent != 0){
    //祖先のカテゴリーを配列で取得
    $ancestors = array_reverse(get_ancestors( $cat -> cat_ID, 'category' ));
    //$ancestorsの配列から一つ一つ$ancestorに取り出してなくなるまでくりかえす
    foreach($ancestors as $ancestor){
      $catName = get_cat_name($ancestor);
      $catUrl = get_category_link($ancestor);
      $i++;
      $catArgs[] = array(
        "@type" => "ListItem",
        "position" => $i,
        "item" => array(
          "@id" => esc_url($catUrl) ,
          "name" => esc_html($catName)
        )
      );
    }
  }
  $i++;
  $currentCatUrl = get_category_link($cat -> term_id);
  $currentCatTitle = $cat-> cat_name;
  $currentCatArgs[] = array(
    "@type" => "ListItem",
    "position" => $i,
    "item" => array(
      "@id" => esc_url($currentCatUrl ) ,
      "name" => esc_html($currentCatTitle)
    )
  );

  if(isset($catArgs)){
    $individualArgs = array_merge($catArgs,$currentCatArgs);
  } else {
    $individualArgs = $currentCatArgs;
  }

} elseif(is_category()){ //カテゴリーアーカイブ
  //パンくずの階層用
  $i=1;
  //カテゴリーに関する情報を取得
  $cat = get_queried_object();
  if($cat -> parent != 0){
      //祖先のカテゴリーを配列で取得
      $ancestors = array_reverse(get_ancestors( $cat -> cat_ID, 'category' ));
      //$ancestorsの配列から一つ一つ$ancestorに取り出してなくなるまでくりかえす
      foreach($ancestors as $ancestor){
        $catName = get_cat_name($ancestor);
        $catUrl = get_category_link($ancestor);
        $i++;
        $individualArgs[] = array(
          "@type" => "ListItem",
          "position" => $i,
          "item" => array(
            "@id" => esc_url($catUrl) ,
            "name" => esc_html($catName)
          )
        );
      }
  }

} elseif(is_page()){ //固定ページ
  //ベージごとに必要な情報のベースを取得。祖先の有無判断に利用。
  $obj = get_queried_object();
  //パンくずの階層用
  $i=1;
  //祖先の固定ページがあれば(0でなければ)分岐
  if($obj -> post_parent != 0){
      //祖先の固定ページを配列で取得
      $pageAncestors = array_reverse( $post -> ancestors );
      //$ancestorsの配列から一つ一つ$ancestorに取り出してなくなるまでくりかえす
      foreach ($pageAncestors as $pageAncestor) {
        $pageName = get_the_title($pageAncestor);
        $pageUrl = get_permalink($pageAncestor);
        $i++;
        $individualArgs[] = array(
          "@type" => "ListItem",
          "position" => $i,
          "item" => array(
            "@id" => esc_url($pageUrl) ,
            "name" => esc_html($pageName)
          )
        );
      }
  }
} elseif(is_date()){ //日別アーカイブ
  //年月日を取得
  $y = get_query_var('year');
  $m = get_query_var('monthnum');
  $d = get_query_var('day');
  //年月日のアーカイブのリンクを取得
  $linkY = get_year_link($y);
  $linkM = get_month_link($y,$m);
  $linkD = get_month_link($y,$m,$d);
  if(is_day()){
    $individualArgs[] = array(
      "@type" => "ListItem",
      "position" => 2,
      "item" => array(
        "@id" => esc_url($linkY) ,
        "name" => esc_html($y)."年"
      ),
      "@type" => "ListItem",
      "position" => 3,
      "item" => array(
        "@id" => esc_url($linkM) ,
        "name" => esc_html($m)."月"
      ),
      "@type" => "ListItem",
      "position" => 4,
      "item" => array(
        "@id" => esc_url($linkD) ,
        "name" => esc_html($d)."日"
      )
    );
  } elseif(is_month()){
    $individualArgs[] = array(
      "@type" => "ListItem",
      "position" => 2,
      "item" => array(
        "@id" => esc_url($linkY) ,
        "name" => esc_html($y)."年"
      ),
      "@type" => "ListItem",
      "position" => 3,
      "item" => array(
        "@id" => esc_url($linkM) ,
        "name" => esc_html($m)."月"
      )
    );
  } elseif(is_year()) {
    $individualArgs[] = array(
      "@type" => "ListItem",
      "position" => 2,
      "item" => array(
        "@id" => esc_url($linkY) ,
        "name" => esc_html($y)."年"
      )
    );
  }
}

/**
 * 最終的にJSON-LDを出力する my_breadcrumb_json_set() に、ホームの情報をまとめたmy_breadcrumb_json_home()を結合して値を渡す。
 * 日別アーカイブはすべて分岐で作成するため、$individualArgs内の情報だけでよい。
 * 日別アーカイブ以外は$individualArgs(ループで取得した祖先の情報)の有無で$itemListElementArgsに渡す内容を変える。
 * @param array $itemListElementArgs
 */
if(is_date()){
  //日別アーカイブ
  $itemListElementArgs = array_merge( my_breadcrumb_json_home(),$individualArgs);
} elseif (isset($individualArgs)){
  //祖先があれば
  $i++;
  $itemListElementArgs =  array_merge( my_breadcrumb_json_home(),$individualArgs,my_breadcrumb_json_current_page($i));
} else {
  //祖先がなければ
  $itemListElementArgs =  array_merge( my_breadcrumb_json_home(),my_breadcrumb_json_current_page(2));
}

$args = array(
  "@context"  => "http://schema.org",
  "@type"     => "BreadcrumbList",
  "itemListElement" => $itemListElementArgs
);


//出力用関数の呼び出し
my_breadcrumb_json_set($args);


JSON-LDのみの実装に関して

実は、今回の切っ掛けは以下のような疑問を感じたためでした。

  • 「HTMLとしてのパンくず」と「JSON-LDのパンくず」の関係

JSON-LDの設定項目には、HTMLに存在するパンくずとの関連を指定する箇所がありません。

JSON-LDのパンくずを紹介する記事では大抵idにbreadcrumbという名称がつけられたHTMLのパンくずがありますが、idなどを自動で認識し関連付けているようにも見えませんでした。

そこでJSON-LDのみを設定したどうなるかという点を、自作したJSON-LDを素材に構造化マークアップテストを使って確かめた次第です。

テストの結果、HTMLのパンくずが存在しなくてもJSON-LDのパンくずさえあれば合格でした。

HTMLのパンくずとJSON-LDの間には、まったく関連が無いようです。

Microdataを引き合いに出してJSON-LDが説明される場合、「マークアップやDOMに手を加えなくてもよい」と書かれる場合がありますが、むしろ「マークアップやDOMと無関係に設定できる」というほうが齟齬がないのかもしれません。

もちろん、知識と経験があれば間違う話ではないと思いますが。

[2018.2.27 追記]

はるかさん(@haruka_pigg)にご指摘をいただきまして、Googleのガイドラインに以下の記述を確認しました。

Don’t mark up content that is not visible to readers of the page. For example, if the JSON-LD markup describes a performer, the HTML body should describe that same performer.

ガイドラインにはHTMLにも同じパンくずの記述が必要とかかれていましたので、たとえ検索結果にパンくずが表示されてもガイドライン違反である、となります。

結び

こうして作ってみたものの、プラグインで実装したほうが安全かつ高機能でおすすめです。

例えば下記のプラグインなどです。

補足

HTMLのパンくずが無いこのブログに、JSON-LDのパンくずだけをこの記事の公開と同時に付けました。

想定ではGoogleの検索結果にパンくずが表示されると思いますが、出た後にどうなるのかはわかりません。

何事も無く表示されつづけるのか、しばらくしたら消えるのか、それとも想定外にそもそも表示されないのか。

[追記]

この記事に限り、検索結果にパンくずが表示されたのを確認しました。

他の記事も再クロールされれば恐らく書き変わるかと思いますが、面倒なので今回はこのまま何もせず様子をみたいと思います。

[追記2]

記事内の「方法2」で実装中。

ただ、JSON-LDは毎回動的に取得しなければならないものではないので、キャッシュに入れるやり方も試したいと思います。

[追記3]

現在は当記事の内容ではなくプラグインで実装中です。

[追記4]

既に「JSON-LDのみの実装に関して」の項で記載していますが、ガイドラインではJSON-LDのみのパンくず設置は不可となっています。

2人がこの記事を評価

役に立ったよという方は上の「記事を評価する」ボタンをクリックしてもらえると嬉しいです。

連投防止のためにCookie使用。SNSへの投稿など他サービスとの連動は一切ありません。

コメント:2件

  1. 大変有用なコードの紹介ありがとうございます。

    私は現在、テーマ制作初心者ながら独自のテーマ制作中で、やっとこさ個別記事やページの構造化データをjson-LDで出力できるようになったところでした。

    ぱんくずリストも・・と思ってはいたものの何をどうやって??という初期段階で挫折しかけたところで貴記事に巡り会えました。

    固定ページがトップページの場合だと出力されてしまうので、貴コードを元にして
    先頭にis_front_page()条件を加え、すぐ下の個別投稿をelseifにして使用させていただいています。

    ありがとうございました。

    1. 記事をお読みいただきありがとうございます。

      json-LDの作例は固定のURLやタイトルを前提としたものが多いと思いますので、「WPのように動的に値を設定するにはどうすればいいのか?」というのは悩みますよね。
      当記事の作例が唯一の正解などではありませんが、今後に活かせる情報となりましたら嬉しい限りです。

コメント欄