
WordPressにはパンくずリストを出力する標準機能がなく、テーマ側で自作するか、プラグインに頼るかのどちらかになります。
この記事ではプラグインを使わずに実装する方法について、自分の理解の整理も兼ねたまとめです。
ここでのやり方は大きく2パターン。1つは「表示させたい階層を配列に手動でセットしてforeachで出力する」方法、もう1つは「ページの種類ごとに条件分岐して自動でパンくずを組み立てる関数を作る」方法です。
パンくずの実装はデータ生成と出力を分けて考える
パンくずの実装は、次の2つの処理に分けて考えると整理しやすいです。
- 今表示しているページの階層を判定して、パンくずの中身(ラベルやリンク先)をデータとして組み立てる処理
- 組み立てたデータを、
<a>タグと<span>タグを出し分けながらHTML(schema.org対応のマークアップ)として出力する処理
この2つを最初から1つの関数に混ぜて書いてしまうと、ページの種類が増えるたびに出力用のHTMLタグまで毎回書き直すことになります。
データを組み立てる部分と、それを出力する部分を分けておくと、出力側のHTML・schema.orgマークアップは1箇所にまとめたまま、ページ種別の判定だけを追加・修正していけます。
①ページの種類を判定してデータを組み立てる
まず、WordPressの条件分岐タグを使って今のページの種類を判定し、$breadcrumbという配列にラベルとリンク先を詰めていきます。配列のキーは、トップページを1番目としたときの通し番号にするので、2番目から詰めていく形です。
$breadcrumb = array();
$i = 2; // トップページが1番目なので2から
/* 通常の投稿ページ */
if(is_singular('post')){
$categories = get_the_category($post->ID);
$cat = $categories[0];
if($cat->parent != 0){
$ancestors = array_reverse(get_ancestors($cat->cat_ID, 'category'));
foreach($ancestors as $ancestor){
$breadcrumb[$i]['label'] = get_cat_name($ancestor);
$breadcrumb[$i]['link'] = get_category_link($ancestor);
$i++;
}
}
$breadcrumb[$i]['label'] = $cat->cat_name;
$breadcrumb[$i]['link'] = get_category_link($cat->term_id);
$i++;
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = get_permalink($post->ID);
}
/* 添付ファイルページ */
elseif(is_attachment()){
if($post->post_parent != 0){
$breadcrumb[$i]['label'] = get_the_title($post->post_parent);
$breadcrumb[$i]['link'] = get_permalink($post->post_parent);
$i++;
}
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = '';
}
/* カスタムポスト */
elseif(is_single() && !is_singular('post')){
$breadcrumb[$i]['label'] = get_post_type_object(get_post_type())->label;
$breadcrumb[$i]['link'] = get_post_type_archive_link(get_post_type());
$i++;
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = get_permalink($post->ID);
}
/* 固定ページ */
elseif(is_page()){
if($post->post_parent != 0){
$ancestors = array_reverse(get_post_ancestors($post->ID));
foreach($ancestors as $ancestor){
$breadcrumb[$i]['label'] = get_the_title($ancestor);
$breadcrumb[$i]['link'] = get_permalink($ancestor);
$i++;
}
}
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = get_permalink($post->ID);
}
/* カテゴリページ */
elseif(is_category()){
$cat = get_queried_object();
if($cat->parent != 0){
$ancestors = array_reverse(get_ancestors($cat->cat_ID, 'category'));
foreach($ancestors as $ancestor){
$breadcrumb[$i]['label'] = get_cat_name($ancestor);
$breadcrumb[$i]['link'] = get_category_link($ancestor);
$i++;
}
}
$breadcrumb[$i]['label'] = $cat->name;
$breadcrumb[$i]['link'] = get_category_link($cat->term_id);
}
/* タグページ */
elseif(is_tag()){
$breadcrumb[$i]['label'] = single_tag_title('', false);
$breadcrumb[$i]['link'] = '';
}
/* 時系列アーカイブページ */
elseif(is_date()){
if(get_query_var('day') != 0){
$breadcrumb[$i]['label'] = get_query_var('year').'年';
$breadcrumb[$i]['link'] = get_year_link(get_query_var('year'));
$i++;
$breadcrumb[$i]['label'] = get_query_var('monthnum').'月';
$breadcrumb[$i]['link'] = get_month_link(get_query_var('year'), get_query_var('monthnum'));
$i++;
$breadcrumb[$i]['label'] = get_query_var('day').'日';
$breadcrumb[$i]['link'] = '';
} elseif(get_query_var('monthnum') != 0){
$breadcrumb[$i]['label'] = get_query_var('year').'年';
$breadcrumb[$i]['link'] = get_year_link(get_query_var('year'));
$i++;
$breadcrumb[$i]['label'] = get_query_var('monthnum').'月';
$breadcrumb[$i]['link'] = '';
} else {
$breadcrumb[$i]['label'] = get_query_var('year').'年';
$breadcrumb[$i]['link'] = '';
}
}
/* 投稿者ページ */
elseif(is_author()){
$breadcrumb[$i]['label'] = '投稿者 : '.get_the_author_meta('display_name', get_query_var('author'));
$breadcrumb[$i]['link'] = '';
}
/* 検索結果ページ */
elseif(is_search()){
$breadcrumb[$i]['label'] = '「'.get_search_query().'」で検索した結果';
$breadcrumb[$i]['link'] = '';
}
/* 404 Not Found ページ */
elseif(is_404()){
$breadcrumb[$i]['label'] = 'お探しの記事は見つかりませんでした。';
$breadcrumb[$i]['link'] = '';
}
/* その他のページ */
else{
$breadcrumb[$i]['label'] = wp_title('', false);
$breadcrumb[$i]['link'] = '';
}条件分岐の内容自体は、判定するページの種類ごとに次のようになっています。
| 条件タグ | 対象ページ | データの組み立て方 |
|---|---|---|
is_singular('post') | 通常の投稿ページ | get_the_category()で投稿のカテゴリを取得し、親カテゴリがあればget_ancestors()で祖先カテゴリをたどってから、カテゴリ→投稿タイトルの順で配列に追加 |
is_single() && !is_singular('post') | カスタム投稿タイプの詳細ページ | get_post_type_object()で投稿タイプ名とアーカイブURLを取得し、投稿タイプ→投稿タイトルの順で配列に追加 |
is_page() | 固定ページ | 親ページがあればget_post_ancestors()で祖先ページをたどってから、ページタイトルを配列に追加 |
is_category() | カテゴリアーカイブ | get_queried_object()で表示中のカテゴリを取得し、投稿ページと同様に祖先カテゴリをたどって追加 |
is_tag() | タグアーカイブ | タグ名のみ追加(リンクなし) |
is_date() | 日付アーカイブ | get_query_var()で年・月・日を取得し、どこまで絞り込まれているかで追加する階層を変える |
is_author() | 投稿者アーカイブ | 投稿者の表示名を追加(リンクなし) |
is_attachment() | 添付ファイルページ | 親記事があればリンク付きで追加してから、添付ファイルのタイトルを追加 |
is_search() | 検索結果ページ | 検索キーワードを追加(リンクなし) |
is_404() | 404ページ | 固定の案内文を追加(リンクなし) |
| 上記以外 | その他のページ | フォールバック |
get_ancestors()・get_post_ancestors()はいずれも祖先IDを「近い順」(一番近い親から順)に配列で返すため、array_reverse()で並びを逆にしてからforeachすることで、パンくずとして自然な「遠い祖先→近い祖先」の順番に並び替えています(get_ancestors()、get_post_ancestors())。
現在のページ(配列の最後の要素)は出力時に<span>で囲みリンクを外すので、linkには空文字を入れていますが、この値は使われません。必須ではありませんが、配列の形をどの分岐でも揃えておくために入れています。
②配列をforeachでHTML(schema.org対応)に出力する
$breadcrumbが組み上がったら、あとはforeachで回して出力するだけです。ポイントになるのが「今処理している要素が配列の最後(=現在のページ)かどうか」の判定です。
if($item !== end($breadcrumb)):end()は配列の最後の要素を返す関数なので、この条件は「今のループの要素が最後の要素と一致しない=まだ途中の階層」という意味になります。条件がtrueの間(現在のページより手前の階層)は<a>タグでリンク付きの<li>を出力し、条件がfalseになったタイミング(=現在のページ)だけ<a>ではなく<span>で囲んでリンクなしの<li>を出力します。現在のページはこれ以上リンクをたどる必要がないので、あえてリンクを外している形です。
itemscopeやitemtype="http://schema.org/BreadcrumbList"、itemprop="position"といった記述は、構造化データ(schema.orgのBreadcrumbList)に対応するためのマークアップです。検索エンジンにパンくずの階層構造を伝える役割があり、Google検索結果でパンくず表示が反映されやすくなります。positionには配列のキー(トップページを1とした通し番号)をそのまま渡せばよいので、foreachのキーをそのまま使っています。
SEOプラグインを使っている場合の重複に注意
All in One SEOなどのSEOプラグインを併用している場合、このitemtype="http://schema.org/BreadcrumbList"のマイクロデータがプラグイン側の出力と重複することがあります。
AIOSEOのドキュメントによると、AIOSEOは「見た目のパンくず表示」と「構造化データとしての出力」を別々に扱っており、BreadcrumbListのJSON-LDはSchema Generatorの機能として自動生成されます。つまり、ショートコードやウィジェットなどでAIOSEO側のパンくずを画面に表示していなくても、スキーマとしてのBreadcrumbListはデフォルトで独立して出力されます。
そのため、テーマ側で上記のようなマイクロデータを出力したまま何もしないと、同じページに「テーマのマイクロデータ」と「AIOSEOのJSON-LD」で2つのBreadcrumbListが存在する状態になります。AIOSEOには「BreadcrumbListスキーマだけをオフにする」設定項目は用意されていないため、外す場合はaioseo_schema_outputフィルターでコードを書いて除去する必要があります。そのため、次のどちらかの対応をしておきたいところです。
- テーマのマイクロデータを残す場合:AIOSEOのスキーマ出力から
aioseo_schema_outputフィルターでBreadcrumbListだけ除外する - AIOSEO側に任せる場合:テーマ側の
itemscope/itemtype/itempropのマイクロデータを外し、見た目のHTMLだけにする
これはAIOSEOに限った話ではなく、Yoast SEOやRank Mathなど他のSEOプラグインでも「表示用の出力」と「スキーマ用の出力」が別々になっていることが多いので、SEOプラグインを使う場合は同じ考え方で確認しておくとよさそうです。
実装まとめ
①と②を合わせると、functions.phpに置くbreadcrumb()は次のようになります。テンプレート側では<?php breadcrumb(); ?>と呼び出すだけで、ページの種類に応じたパンくずがschema.org対応のマークアップ付きで出力されます。
function breadcrumb(){
global $post;
$breadcrumb = array();
$i = 2; // トップページが1番目なので2から
if(is_singular('post')){
$categories = get_the_category($post->ID);
$cat = $categories[0];
if($cat->parent != 0){
$ancestors = array_reverse(get_ancestors($cat->cat_ID, 'category'));
foreach($ancestors as $ancestor){
$breadcrumb[$i]['label'] = get_cat_name($ancestor);
$breadcrumb[$i]['link'] = get_category_link($ancestor);
$i++;
}
}
$breadcrumb[$i]['label'] = $cat->cat_name;
$breadcrumb[$i]['link'] = get_category_link($cat->term_id);
$i++;
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = get_permalink($post->ID);
}
elseif(is_single() && !is_singular('post')){
$breadcrumb[$i]['label'] = get_post_type_object(get_post_type())->label;
$breadcrumb[$i]['link'] = home_url('/').get_post_type_object(get_post_type())->name;
$i++;
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = get_permalink($post->ID);
}
elseif(is_page()){
if($post->post_parent != 0){
$ancestors = array_reverse(get_post_ancestors($post->ID));
foreach($ancestors as $ancestor){
$breadcrumb[$i]['label'] = get_the_title($ancestor);
$breadcrumb[$i]['link'] = get_permalink($ancestor);
$i++;
}
}
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = get_permalink($post->ID);
}
elseif(is_category()){
$cat = get_queried_object();
if($cat->parent != 0){
$ancestors = array_reverse(get_ancestors($cat->cat_ID, 'category'));
foreach($ancestors as $ancestor){
$breadcrumb[$i]['label'] = get_cat_name($ancestor);
$breadcrumb[$i]['link'] = get_category_link($ancestor);
$i++;
}
}
$breadcrumb[$i]['label'] = $cat->name;
$breadcrumb[$i]['link'] = get_category_link($cat->term_id);
}
elseif(is_tag()){
$breadcrumb[$i]['label'] = single_tag_title('', false);
$breadcrumb[$i]['link'] = '';
}
elseif(is_date()){
if(get_query_var('day') != 0){
$breadcrumb[$i]['label'] = get_query_var('year').'年';
$breadcrumb[$i]['link'] = get_year_link(get_query_var('year'));
$i++;
$breadcrumb[$i]['label'] = get_query_var('monthnum').'月';
$breadcrumb[$i]['link'] = get_month_link(get_query_var('year'), get_query_var('monthnum'));
$i++;
$breadcrumb[$i]['label'] = get_query_var('day').'日';
$breadcrumb[$i]['link'] = '';
} elseif(get_query_var('monthnum') != 0){
$breadcrumb[$i]['label'] = get_query_var('year').'年';
$breadcrumb[$i]['link'] = get_year_link(get_query_var('year'));
$i++;
$breadcrumb[$i]['label'] = get_query_var('monthnum').'月';
$breadcrumb[$i]['link'] = '';
} else {
$breadcrumb[$i]['label'] = get_query_var('year').'年';
$breadcrumb[$i]['link'] = '';
}
}
elseif(is_author()){
$breadcrumb[$i]['label'] = '投稿者 : '.get_the_author_meta('display_name', get_query_var('author'));
$breadcrumb[$i]['link'] = '';
}
elseif(is_attachment()){
if($post->post_parent != 0){
$breadcrumb[$i]['label'] = get_the_title($post->post_parent);
$breadcrumb[$i]['link'] = get_permalink($post->post_parent);
$i++;
}
$breadcrumb[$i]['label'] = $post->post_title;
$breadcrumb[$i]['link'] = '';
}
elseif(is_search()){
$breadcrumb[$i]['label'] = '「'.get_search_query().'」で検索した結果';
$breadcrumb[$i]['link'] = '';
}
elseif(is_404()){
$breadcrumb[$i]['label'] = 'お探しの記事は見つかりませんでした。';
$breadcrumb[$i]['link'] = '';
}
else{
$breadcrumb[$i]['label'] = wp_title('', false);
$breadcrumb[$i]['link'] = '';
}
?>
<section id="breadcrumb" class="section controller">
<div class="controller-inner clearfix">
<ol itemscope itemtype="http://schema.org/BreadcrumbList" class="breadcrumb-list">
<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" class="home">
<a itemscope itemtype="http://schema.org/Thing" itemprop="item" href="<?php echo home_url('/'); ?>">
<span itemprop="name">HOME</span>
</a>
<meta itemprop="position" content="1" />
</li>
<?php foreach($breadcrumb as $nums => $item): ?>
<?php if($item !== end($breadcrumb)): ?>
<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">
<a itemscope itemtype="http://schema.org/Thing" itemprop="item" href="<?php echo $item['link']; ?>" itemid="<?php echo $item['link']; ?>">
<span itemprop="name"><?php echo $item['label']; ?></span>
</a>
<meta itemprop="position" content="<?php echo $nums; ?>" />
</li>
<?php else: ?>
<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" class="on">
<span itemscope itemtype="http://schema.org/Thing" itemprop="item">
<span itemprop="name"><?php echo $item['label']; ?></span>
</span>
<meta itemprop="position" content="<?php echo $nums; ?>" />
</li>
<?php endif; ?>
<?php endforeach; ?>
</ol>
</div><!-- controller-inner -->
</section><!-- breadcrumb -->
<?php
}カテゴリー : WordPress