【WordPress】カスタム投稿タイプ、リッチスニペットに対応したパンくずリストを表示する

  • 更新日:
  • 公開日:

2015年5月15日追記:

新たにリッチスニペットやカスタム投稿に対応した、多様なオプション設定できるパンくずリストを自作してみました。


プラグインを使わずにWordPressサイトにパンくずリストを表示するソースコードです。

カスタム投稿タイプ、リッチスニペットに対応していますが、カスタム投稿タイプの年月日アーカイブに関してはパーマリンク関係が面倒なため、プラグイン『Custom Post Type Permalinks』を有効化していること前提のソースコードになっているので注意です。(このプラグインを有効化しているとカスタム投稿タイプのパーマリンクを最適化してくれるので便利です)

参考になったページ

ソースコードの前に参考になったページをご紹介。参考になったどころか殆どの部分を流用しているので…。以下のページのソースコードを書き換えさせて頂きました。

私もWebデザインレシピさんの記事『パンくずリストを作ってみるとWordPress でのサイト構築のコツがつかめるかもしれない(コード 付き)』に記述されてあるソースコードを書き換えて使っていたのですが、カスタム投稿タイプに対応したソースコードが無いかと調べていたら見つかった記事です。

Web Design Leavesさん、Webデザインレシピさんに感謝!(地面に頭を付けて土下座しながら)

パンくずを表示するソースコード

それではソースコードについて。以下に表示する3つの関数をfunctions.phpに貼り付けて使用します。

まずはパンくずリスト表示用の関数です。

//パンくずリストを出力する関数
function breadcrumbs($args = array()){
    global $post;
    $str ='';
    $defaults = array(
        'id' => "breadcrumbs",
        'class' => "",
        'home' => "Home",
        'search' => "で検索した結果",
        'tag' => "タグ",
        'author' => "投稿者",
        'notfound' => "404 Not found",
        'separator' => '',   //デフォルトは「 > 」
        'textContainer' => 'strong',
        'liOption' => ' itemscope itemtype="http://data-vocabulary.org/Breadcrumb"',
        'aOption' => ' itemprop="url"',
        'spanOption' => ' itemprop="title"'
    );
    $args = wp_parse_args( $args, $defaults );
    extract( $args, EXTR_SKIP );

    //セパレータが設定されてない場合は表示なし(空白)
    if($separator == '') {
        $separatorHTML = $separator;
    } else {
        $separatorHTML = '<li>'.$separator.'</li>';
    }
    if($textContainer == '') {
        $startTextContainer =  '';
        $endTextContainer = '';
    } else {
        $startTextContainer =  '<' . $textContainer . '>';
        $endTextContainer =  '</' . $textContainer . '>';
    }

    if(is_home()) {
        echo  '<div id="'. $id .'" class="' . $class.'">'.'<ol itemprop="breadcrumb"><li class="bcHome"' . $liOption . '><span' . $spanOption . '>'. $home .'</span></li></ol></div>';
    }
    if(!is_home()&&!is_admin()){ //!is_admin は管理ページ以外という条件分岐
        $str.= '<div id="'. $id .'" class="' . $class.'">';
        $str.= '<ol itemprop="breadcrumb">';
        $str.= '<li class="bcHome"' . $liOption . '><a href="'. home_url() .'/"' . $aOption . '><span' . $spanOption . '>'. $home .'</span></a></li>';
        $str.= $separatorHTML;
        $my_taxonomy = get_query_var('taxonomy');  //[taxonomy] の値(タクソノミーのスラッグ)
        $cpt = get_query_var('post_type');   //[post_type] の値(投稿タイプ名)
        if($my_taxonomy &&  is_tax($my_taxonomy)) {//カスタム分類のページ
            $my_tax = get_queried_object();  //print_r($my_tax);
            $post_types = get_taxonomy( $my_taxonomy )->object_type;
            $cpt = $post_types[0];  //カスタム分類名からカスタム投稿名を取得。
            $str.='<li' . $liOption . '><a href="' .get_post_type_archive_link($cpt).'"' . $aOption . '><span' . $spanOption . '>'. get_post_type_object($cpt)->label.'</span></a></li>';  //カスタム投稿のアーカイブへのリンクを出力
            $str.= $separatorHTML;
            if($my_tax -> parent != 0) {  //親があればそれらを取得して表示
                $ancestors = array_reverse(get_ancestors( $my_tax -> term_id, $my_tax->taxonomy ));
                foreach($ancestors as $ancestor){
                    $str.='<li' . $liOption . '><a href="'. get_term_link($ancestor, $my_tax->taxonomy) .'"' . $aOption . '><span' . $spanOption . '>'. get_term($ancestor, $my_tax->taxonomy)->name .'</span></a></li>';
                    $str.= $separatorHTML;
                }
            }
            $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer . $my_tax -> name . $endTextContainer . '</span></li>';
        }elseif(is_category()) {  //カテゴリーのアーカイブページ
            $cat = get_queried_object();
            if($cat -> parent != 0){
                $ancestors = array_reverse(get_ancestors( $cat -> cat_ID, 'category' ));
                foreach($ancestors as $ancestor){
                    $str.='<li' . $liOption . '><a href="'. get_category_link($ancestor) .'"' . $aOption . '><span' . $spanOption . '>'. get_cat_name($ancestor) .'</span></a></li>';
                    $str.= $separatorHTML;
                }
            }
            $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. $cat -> name . $endTextContainer . '</span></li>';
        }elseif(is_post_type_archive()) {  //カスタム投稿のアーカイブページ
            $cpt = get_query_var('post_type');
            if(is_date()) { //年月日アーカイブだった場合
                $str.='<li' . $liOption . '><a href="' .get_post_type_archive_link($cpt).'"' . $aOption . '><span' . $spanOption . '>'. get_post_type_object($cpt)->label.'</span></a></li>';
                if(get_query_var('day') != 0){  //日別アーカイブ
                    $postTyleLink = get_post_type_archive_link(get_post_type());
                    $str.='<li' . $liOption . '><a href="'. $postTyleLink . 'date/'  . get_query_var('year') . '/"' . $aOption . '><span' . $spanOption . '>' . get_query_var('year'). '年</span></a></li>';
                    $str.= $separatorHTML;
                    $str.='<li' . $liOption . '><a href="'. $postTyleLink . 'date/'  . get_query_var('year') . '/'  . get_query_var('monthnum'). '/"' . $aOption . '><span' . $spanOption . '>'. get_query_var('monthnum') .'月</span></a></li>';
                    $str.= $separatorHTML;
                    $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. get_query_var('day'). '日</strong></span></li>';
                } elseif(get_query_var('monthnum') != 0){  //月別アーカイブ
                    $postTyleLink = get_post_type_archive_link(get_post_type());
                    $str.='<li' . $liOption . '><a href="'. $postTyleLink . 'date/'  . get_query_var('year') . '/"' . $aOption . '><span' . $spanOption . '>'. get_query_var('year') .'年</span></a></li>';
                    $str.= $separatorHTML;
                    $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. get_query_var('monthnum'). '月</strong></span></li>';
                } else {  //年別アーカイブ
                    $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. get_query_var('year') .'年</strong></span></li>';
                }
            } else {
                $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. get_post_type_object($cpt)->label . $endTextContainer . '</span></li>';
            }
        }elseif($cpt && is_singular($cpt)){  //カスタム投稿の個別記事ページ
            $taxes = get_object_taxonomies( $cpt  );
            $mytax = $taxes[0];
            $str.='<li' . $liOption . '><a href="' .get_post_type_archive_link($cpt).'"' . $aOption . '><span' . $spanOption . '>'. get_post_type_object($cpt)->label.'</span></a></li>';  //カスタム投稿のアーカイブへのリンクを出力
            $str.= $separatorHTML;
            $taxes = get_the_terms($post->ID, $mytax);
            $tax = $taxes ? get_youngest_tax($taxes, $mytax ) : null;
            if( !empty($tax) ) {
                if($tax -> parent != 0){
                    $ancestors = array_reverse(get_ancestors( $tax -> term_id, $mytax ));
                        foreach($ancestors as $ancestor){
                            $str.='<li' . $liOption . '><a href="'. get_term_link($ancestor, $mytax).'"' . $aOption . '><span' . $spanOption . '>'. get_term($ancestor, $mytax)->name . '</span></a></li>';
                            $str.= $separatorHTML;
                        }
                }
                $str.='<li' . $liOption . '><span' . $spanOption . '>'. $tax->name . '</span></li>';
                $str.= $separatorHTML;
            }
            $str.='<li' . $liOption . '><a href="'. get_term_link($tax, $mytax).'"' . $aOption . '><span' . $spanOption . '>'. $tax -> name . '</span></a></li>';
            $str.= $separatorHTML;
            $str.= '<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. $post -> post_title .$endTextContainer . '</span></li>';
        }elseif(is_single()){  //個別記事ページ
            $categories = get_the_category($post->ID);
            $cat = get_youngest_cat($categories);
            if($cat -> parent != 0){
                $ancestors = array_reverse(get_ancestors( $cat -> cat_ID, 'category' ));
                foreach($ancestors as $ancestor){
                    $str.='<li' . $liOption . '><a href="'. get_category_link($ancestor).'"' . $aOption . '><span' . $spanOption . '>'. get_cat_name($ancestor). '</span></a></li>';
                    $str.= $separatorHTML;
                }
            }
            $str.='<li' . $liOption . '><a href="'. get_category_link($cat -> term_id). '"' . $aOption . '><span' . $spanOption . '>'. $cat-> cat_name . '</span></a></li>';
            $str.= $separatorHTML;
            $str.= '<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. $post -> post_title .$endTextContainer . '</span></li>';
        } elseif(is_page()){  //固定ページ
            if($post -> post_parent != 0 ){
                $ancestors = array_reverse(get_post_ancestors( $post->ID ));
                foreach($ancestors as $ancestor){
                    $str.='<li' . $liOption . '><a href="'. get_permalink($ancestor).'"' . $aOption . '><span' . $spanOption . '>'. get_the_title($ancestor) .'</span></a></li>';
                    $str.= $separatorHTML;
                }
            }
            $str.= '<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. $post -> post_title .$endTextContainer . '</span></li>';
        } elseif(is_date()){  //日付ベースのアーカイブページ
            if(get_query_var('day') != 0){  //年別アーカイブ
                $str.='<li' . $liOption . '><a href="'. get_year_link(get_query_var('year')). '"' . $aOption . '><span' . $spanOption . '>' . get_query_var('year'). '年</span></a></li>';
                $str.= $separatorHTML;
                $str.='<li' . $liOption . '><a href="'. get_month_link(get_query_var('year'), get_query_var('monthnum')). '"' . $aOption . '><span' . $spanOption . '>'. get_query_var('monthnum') .'月</span></a></li>';
                $str.= $separatorHTML;
                $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. get_query_var('day'). '日</strong></span></li>';
            } elseif(get_query_var('monthnum') != 0){  //月別アーカイブ
                $str.='<li' . $liOption . '><a href="'. get_year_link(get_query_var('year')) .'"' . $aOption . '><span' . $spanOption . '>'. get_query_var('year') .'年</span></a></li>';
                $str.= $separatorHTML;
                $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. get_query_var('monthnum'). '月</strong></span></li>';
            } else {  //年別アーカイブ
                $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. get_query_var('year') .'年</strong></span></li>';
            }
        } elseif(is_search()) {  //検索結果表示ページ
            $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. '「'. get_search_query() .'」'. $search .$endTextContainer . '</span></li>';
        } elseif(is_author()){  //投稿者のアーカイブページ
            $str .='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. $author .' : '. get_the_author_meta('display_name', get_query_var('author')).$endTextContainer . '</span></li>';
        } elseif(is_tag()){  //タグのアーカイブページ
            $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. $tag .' : '. single_tag_title( '' , false ). $endTextContainer . '</span></li>';
        } elseif(is_attachment()){  //添付ファイルページ
            $str.= '<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. $post -> post_title .$endTextContainer . '</span></li>';
        } elseif(is_404()){  //404 Not Found ページ
            $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer.$notfound.$endTextContainer . '</span></li>';
        } else{  //その他
            $str.='<li' . $liOption . '><span' . $spanOption . '>' . $startTextContainer. wp_title('', true) .$endTextContainer . '</span></li>';
        }
        $str.='</ol>';
        $str.='</div>';
    }
    echo $str;
}

次に最下層のカテゴリーを返す関数です。

//一番下の階層のカテゴリーを返す関数
function get_youngest_cat($categories){
    global $post;
    if(count($categories) == 1 ){
        $youngest = $categories[0];
    }
    else{
        $count = 0;
        foreach($categories as $category){  //それぞれのカテゴリーについて調査
            $children = get_term_children( $category -> term_id, 'category' );  //子カテゴリーの ID を取得
            if($children){  //子カテゴリー(の ID )が存在すれば
                if ( $count < count($children) ){  //子カテゴリーの数が多いほど、そのカテゴリーは階層が下なのでそれを元に調査するかを判定
                    $count = count($children);  //$count に子カテゴリーの数を代入
                    $lot_children = $children;
                    foreach($lot_children as $child){  //それぞれの「子カテゴリー」について調査 $childは子カテゴリーのID
                        if( in_category( $child, $post -> ID ) ){  //現在の投稿が「子カテゴリー」のカテゴリーに属するか
                            $youngest = get_category($child);  //属していればその「子カテゴリー」が一番若い(一番下の階層)
                        }
                    }
                }
            }
            else{  //子カテゴリーが存在しなければ
                $youngest = $category;  //そのカテゴリーが一番若い(一番下の階層)
            }
        }
    }
    return $youngest;
}

最後に最下層のタクソノミーを返す関数です。

//一番下の階層のタクソノミーを返す関数
function get_youngest_tax($taxes, $mytaxonomy){
    global $post;
    if(count($taxes) == 1 ){
        $youngest = $taxes[key($taxes)];
    }
    else{
        $count = 0;
        foreach($taxes as $tax){  //それぞれのタクソノミーについて調査
            $children = get_term_children( $tax -> term_id, $mytaxonomy );  //子タクソノミーの ID を取得
            if($children){  //子カテゴリー(の ID )が存在すれば
                if ( $count < count($children) ){  //子タクソノミーの数が多いほど、そのタクソノミーは階層が下なのでそれを元に調査するかを判定
                    $count = count($children);  //$count に子タクソノミーの数を代入
                    $lot_children = $children;
                    foreach($lot_children as $child){  //それぞれの「子タクソノミー」について調査 $childは子タクソノミーのID
                        if( is_object_in_term( $post -> ID, $mytaxonomy ) ){  //現在の投稿が「子タクソノミー」のタクソノミーに属するか
                            $youngest = get_term($child, $mytaxonomy);  //属していればその「子タクソノミー」が一番若い(一番下の階層)
                        }
                    }
                }
            }
            else{  //子タクソノミーが存在しなければ
                $youngest = $tax;  //そのタクソノミーが一番若い(一番下の階層)
            }
        }
    }
    return $youngest;
}

使い方

上記3つの関数をfunctions.phpに記述し、パンくずリストを表示したいテンプレートに下の1行を貼り付けて使います。

<?php breadcrumbs(); ?>

引数に配列でパラメータを渡せる仕組みになっています。指定方法についてはソースコードの参考ページをご覧下さい。

付け加えた4つの機能

参考にしたソースコードに私が付け加えた機能は

  • セパレータの有無の設定
  • 最下層のliタグのテキストを任意のタグで囲む設定
  • カスタム投稿タイプの年月日アーカイブ時のリンク出力
  • リッチスニペットをタグに追加出来るパラメータ

の4つです。

セパレータの有無の設定

デフォルトだとセパレータ(区切りのタグ)は表示する設定になっていますが、もし区切りタグを表示したくない場合は以下のように記述します。

<?php
$args = array(
    'separator' => '' //空白のパラメータを渡すと非表示
);
breadcrumbs($args);
?>

私の場合、パンくずリストの区切りはCSSの擬似要素「:before」で表示していたので、パラメータを渡すことでタグを出力しないように出来る機能を付け加えました。

最下層のliタグのテキストを任意のタグで囲む設定

この機能は主にパンくずリストの最下層にstrongタグを追加するか、しないかのために付けました。もしstrongタグで囲まない場合は以下のように記述します。(strongタグ以外にも「em」と書けばemタグでテキストを囲めます)

<?php
$args = array(
    'textContainer' => '' //空白にするとタグ消去
);
breadcrumbs($args);
?>

なぜ最下層のテキストにstrongタグを付けるのかといえば、SEO対策の本を読んで以来私が盲目的に行っているからです。

この記事を書いている最中に「でもブログだったら記事中にstrongタグ入れたほうが良いのかな?」って思ったので臨時で追加した機能です。(strongタグについてはこちらの記事『【あなたのサイトはスパムです】SEO対策における< strong>タグの使い方』が参考になります)

カスタム投稿タイプの年月日アーカイブ時のリンク出力

カスタム投稿タイプの年月日アーカイブ時にリンクを出力するようにしています。ただし、冒頭でも述べたとおり「Custom Post Type Permalinks」を有効化していなければリンクがおかしくなるので注意して下さい。

インストールは簡単で、管理パネルの「プラグイン > 新規追加」から検索すると見つかるのでこちらからダウンロードします。有効化するだけで年月日アーカイブのパーマリンクを最適化してくれます。(もし有効化してもリンクがおかしい場合はパーマリンクの設定を上書き保存してみて下さい)

カスタム投稿タイプを実装していない場合、このプラグインは入れなくても問題なく動くと思うので大丈夫です。

リッチスニペットをタグに追加出来るパラメータ

リッチスニペットと言ってますが、実際の所リッチスニペット以外の属性を追加出来ます。(全てのli、a、spanタグ各々に属性、というか文字列を追加出来る仕様です。あまり動的な値は指定出来ませんが^^;)

リッチスニペットは「Data-Vocabulary.org」に対応した設定を予め記述してあります(デフォルトのパラメータに指定してあります)。もしリッチスニペットを外したい場合は以下のようにパラメータを渡してください。

<?php
$args = array(
    'liOption' => '',
    'aOption' => '',
    'spanOption' => ''
);
breadcrumbs($args);
?>

Schema.org」用の記述もしてありますが、schema.orgに対応するにはサイトのテンプレートに追加で必要な記述があります。私の例ですが、bodyタグのソースコードを以下のようにしています。

<body <?php body_class(); ?> itemscope itemtype="http://schema.org/WebPage" itemref="breadcrumbs">

schema.orgのパンくずリストのリッチスニペットは、「http://schema.org/WebPage」で囲まれた中のパンくずリストのタグに itemprop=”breadcrumb”を記述するだけでOKです(今の所は)。「itemref」にパンくずリストのolタグに指定してあるID「breadcrumbs」を入力することで参照しています。

リッチスニペットについてちょっと余談

余談ですが、Data-Vocabulary.orgは既にサービスが終了していて、現在はschema.orgがそのサービスを引き継いているようです。

何が言いたいかというと、「Data-Vocabulary.org」は既に終了し、役割は「スキーマ」に引き継がれているということです。Googleが今までサポートしてきた「Data-Vocabulary.org」の記法をいきなり無効にするようなことはないでしょうが、今後は「スキーマ」が主流になるはずです。

「だから安心してスキーマ学ぼうぜ!」ってことですねー。

初心者向けスキーマ(Schema.org)!マークアップを解説!

しかし、Googleの検索結果ではまだData-Vocabulary.orgが認識されているのが現状です。なのでソースコードでは両方の記述をしています。将来修正する面倒を省くことも考えてパラメータで消せるようにしておきました。

photo credit: shutterbean via photopincc

書いた人

Symbol Mark

Ryoichi(しつ)

除菌ティッシュを買い込んで使いきれずによく乾かす人。

療養目的で退職し、どうやって生きていくか模索中。最近は勉強目的でLaravelやVue.js弄ったり、趣味で音で遊んでます。

※2019年10月16日現在ブログリニューアル中です。崩れなどが発生していたらすみません。

うぇぶ: @s_ryone