2026年7月11日

2026年7月11日

WordPressのテーマでパンくずリストをプラグインなしで独自実装する方法

パンくずリストをプラグインなしで作るなら、「表示中のページがどの階層にあるか」をWordPressの条件分岐タグで判定し、ホームから現在地までのリンクを配列に組み立ててから出力する、という2段階に分けて実装するのが分かりやすい方法です。この記事では、投稿・固定ページ・カテゴリー・カスタム投稿タイプなどに対応した関数と、クラシックテーマ/ブロックテーマへの組み込み方、構造化データを付けるときの注意点までを解説します。

独自実装とプラグイン、どちらを選ぶか

Yoast SEO や Rank Math などのSEOプラグインには、パンくずリストの機能が含まれています。すでにこれらを使っていて、表示形式に不満がなければ、その機能を使うほうが保守の手間は少なくなります。

独自実装が向いているのは、SEOプラグインを使っていない、カテゴリーの扱いや表示文言を細かく決めたい、テーマのマークアップに合わせたい、といった場合です。自分で書くぶん、新しい投稿タイプやタクソノミーを追加したときには関数の更新が必要になります。

作業前の準備

コードは子テーマの functions.php に書きます。親テーマに書くとテーマ更新で消えるためです。functions.php の書き間違いはサイト全体の表示を止めてしまうので、編集前にバックアップを取り、FTPやファイルマネージャーで元に戻せる状態にしておきます。

手順1:パンくずの項目を配列で組み立てる

まず、表示中のページに応じて「ラベル」と「URL」の組を順に並べた配列を返す関数を作ります。HTMLの出力と分けておくと、後で構造化データを作るときにも同じ配列を使えます。

<?php
// 子テーマの functions.php
function mytheme_breadcrumb_items() {
	if ( is_front_page() ) {
		return array(); // トップページでは表示しない
	}

	$items   = array();
	$items[] = array( 'label' => 'ホーム', 'url' => home_url( '/' ) );

	if ( is_home() ) {
		// 「投稿ページ」に指定した固定ページ
		$items[] = array( 'label' => get_the_title( get_option( 'page_for_posts' ) ), 'url' => '' );

	} elseif ( is_singular( 'post' ) ) {
		$categories = get_the_category( get_queried_object_id() );
		if ( $categories ) {
			$category = $categories[0];
			$parents  = array_reverse( get_ancestors( $category->term_id, 'category', 'taxonomy' ) );
			foreach ( $parents as $parent_id ) {
				$items[] = array( 'label' => get_cat_name( $parent_id ), 'url' => get_category_link( $parent_id ) );
			}
			$items[] = array( 'label' => $category->name, 'url' => get_category_link( $category->term_id ) );
		}
		$items[] = array( 'label' => get_the_title( get_queried_object_id() ), 'url' => '' );

	} elseif ( is_page() ) {
		$parents = array_reverse( get_post_ancestors( get_queried_object_id() ) );
		foreach ( $parents as $parent_id ) {
			$items[] = array( 'label' => get_the_title( $parent_id ), 'url' => get_permalink( $parent_id ) );
		}
		$items[] = array( 'label' => get_the_title( get_queried_object_id() ), 'url' => '' );

	} elseif ( is_singular() ) {
		// カスタム投稿タイプの個別ページ
		$post_type = get_post_type_object( get_post_type( get_queried_object_id() ) );
		if ( $post_type && $post_type->has_archive ) {
			$items[] = array( 'label' => $post_type->labels->name, 'url' => get_post_type_archive_link( $post_type->name ) );
		}
		$items[] = array( 'label' => get_the_title( get_queried_object_id() ), 'url' => '' );

	} elseif ( is_category() || is_tag() || is_tax() ) {
		$term = get_queried_object();
		if ( is_taxonomy_hierarchical( $term->taxonomy ) ) {
			$parents = array_reverse( get_ancestors( $term->term_id, $term->taxonomy, 'taxonomy' ) );
			foreach ( $parents as $parent_id ) {
				$parent = get_term( $parent_id, $term->taxonomy );
				if ( ! $parent instanceof WP_Term ) {
					continue;
				}
				$link = get_term_link( $parent );
				if ( ! is_wp_error( $link ) ) {
					$items[] = array( 'label' => $parent->name, 'url' => $link );
				}
			}
		}
		$items[] = array( 'label' => $term->name, 'url' => '' );

	} elseif ( is_post_type_archive() ) {
		$items[] = array( 'label' => post_type_archive_title( '', false ), 'url' => '' );

	} elseif ( is_search() ) {
		$items[] = array( 'label' => sprintf( '「%s」の検索結果', get_search_query( false ) ), 'url' => '' );

	} elseif ( is_404() ) {
		$items[] = array( 'label' => 'ページが見つかりません', 'url' => '' );

	} elseif ( is_archive() ) {
		// 投稿者・日付アーカイブなど
		$items[] = array( 'label' => wp_strip_all_tags( get_the_archive_title() ), 'url' => '' );
	}

	return $items;
}

組み立てで押さえておきたい点は次のとおりです。

  • 親の並び順: get_ancestors() と get_post_ancestors() は「近い親から順」に返すため、array_reverse() で「最上位の親から順」に並べ替えています。
  • 投稿が複数カテゴリーに属する場合: 上の例では get_the_category() の最初の1件を使っています。どのカテゴリーが先頭になるかは並び順の指定に左右されるため、「主カテゴリー」を決めたい場合は、カスタムフィールドに主カテゴリーのIDを保存して優先する、といった独自のルールを足します。
  • 条件分岐の順番: is_singular() はすべての個別ページで真になるため、投稿・固定ページの判定より後に置いています。同様に is_archive() はカテゴリーや投稿タイプのアーカイブでも真になるので、最後に回しています。

手順2:HTMLとして出力する

次に、配列をHTMLに変換する関数を作ります。

<?php
function mytheme_breadcrumb() {
	$items = mytheme_breadcrumb_items();
	if ( count( $items ) < 2 ) {
		return '';
	}

	$last = count( $items ) - 1;
	$html = '<nav class="breadcrumb" aria-label="パンくずリスト"><ol class="breadcrumb__list">';
	foreach ( $items as $i => $item ) {
		if ( $i === $last || '' === $item['url'] ) {
			$current = ( $i === $last ) ? ' aria-current="page"' : '';
			$html   .= '<li class="breadcrumb__item"><span' . $current . '>' . esc_html( $item['label'] ) . '</span></li>';
		} else {
			$html .= '<li class="breadcrumb__item"><a href="' . esc_url( $item['url'] ) . '">' . esc_html( $item['label'] ) . '</a></li>';
		}
	}
	$html .= '</ol></nav>';

	return $html;
}
add_shortcode( 'mytheme_breadcrumb', 'mytheme_breadcrumb' );

マークアップは nav 要素に aria-label を付け、順序に意味があるので ol(番号付きリスト)を使います。現在のページはリンクにせず、aria-current="page" を付けておくと、スクリーンリーダーでも現在地が伝わります。ラベルとURLは出力の直前で esc_html() と esc_url() を通しています。検索キーワードのように訪問者が入力した値もラベルに入るため、エスケープは省略しないでください。

区切り記号はHTMLに書かず、CSSで付けると管理が楽です。

.breadcrumb__list {
	display: flex;
	flex-wrap: wrap;
	margin: 0 0 1.5em;
	padding: 0;
	list-style: none;
	font-size: 13px;
}
.breadcrumb__item + .breadcrumb__item::before {
	content: "›";
	margin: 0 0.5em;
	color: #888;
}

手順3:テーマに組み込む

クラシックテーマでは、親テーマの header.php を子テーマにコピーし、ヘッダーの直後など表示したい位置に次の1行を追加します。特定のテンプレート(single.php など)だけに出したい場合は、そのファイルをコピーして書きます。

<?php echo mytheme_breadcrumb(); ?>

ブロックテーマでは、サイトエディターで表示したいテンプレート(「単一投稿」「ページ」「アーカイブ」など)を開き、「ショートコード」ブロックを置いて [mytheme_breadcrumb] と入力します。複数のテンプレートに共通で出したいなら、ヘッダーのテンプレートパーツに入れる方法もあります。トップページでは関数が空を返すので、ヘッダーに置いても余計な表示は出ません。

構造化データ(BreadcrumbList)を付ける場合

検索結果にパンくずを表示させたい場合は、schema.org の BreadcrumbList を JSON-LD で出力します。手順1の配列をそのまま使えます。

<?php
add_action( 'wp_head', function () {
	$items = mytheme_breadcrumb_items();
	if ( count( $items ) < 2 ) {
		return;
	}

	$list = array();
	foreach ( $items as $i => $item ) {
		$element = array(
			'@type'    => 'ListItem',
			'position' => $i + 1,
			// タイトルに含まれる文字参照(&#8217; など)を元の文字に戻す
			'name'     => html_entity_decode( wp_strip_all_tags( $item['label'] ), ENT_QUOTES, get_bloginfo( 'charset' ) ),
		);
		if ( '' !== $item['url'] ) {
			$element['item'] = $item['url'];
		}
		$list[] = $element;
	}

	$data = array(
		'@context'        => 'https://schema.org',
		'@type'           => 'BreadcrumbList',
		'itemListElement' => $list,
	);
	echo '<script type="application/ld+json">' . wp_json_encode( $data ) . '</script>' . "\n";
} );

注意したいのは重複です。Yoast SEO や Rank Math などのSEOプラグインは、それ自体が BreadcrumbList を含む構造化データを出力する場合があります。同じページに内容の異なる BreadcrumbList が2つあると、検索エンジンがどちらを採用するか分かりにくくなるため、プラグイン側の出力を確認し、どちらか一方にまとめてください。

動作確認のポイント

  • 投稿・固定ページ(親子関係あり)・カテゴリー(親子関係あり)・タグ・検索結果・存在しないURLを開き、それぞれ想定どおりの階層が出るかを確認します。
  • 構造化データを出力した場合は、Google のリッチリザルトテストで、BreadcrumbList がエラーなく認識されるかを確かめます(2026年10月現在のツール名です)。
  • ページキャッシュを使っている場合は、変更後にキャッシュを削除してからログアウト状態で確認します。
  • 表示が崩れたり画面が真っ白になったりした場合は、WP_DEBUG_LOG を有効にして wp-content/debug.log を確認し、必要ならバックアップした functions.php に戻します。

まとめ

  • パンくずリストは「項目の配列を作る関数」と「HTMLに変換する関数」に分けると、表示と構造化データの両方に使い回せます。
  • 親の取得には get_ancestors() と get_post_ancestors() を使い、array_reverse() で上位から並べます。条件分岐は範囲の狭いものから順に書きます。
  • 出力は nav と ol でマークアップし、現在地に aria-current="page" を付け、ラベルとURLは出力時にエスケープします。
  • クラシックテーマは header.php などに1行追加、ブロックテーマはショートコードブロックで組み込みます。SEOプラグインと構造化データが重複しないようにしてください。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲