2026年8月19日

2026年8月19日

WordPressの階層型カスタム投稿タイプで親子構造のコンテンツを管理する方法

マニュアルや製品ドキュメント、施設案内のように「親ページの下に子ページがぶら下がる」コンテンツは、register_post_type() に 'hierarchical' => true を指定した階層型カスタム投稿タイプで管理すると扱いやすくなります。この記事では、登録コードの書き方、親子関係を前提にしたURL設計、子ページ一覧やパンくずの出し方、運用上の注意点までを順に説明します。読み終えるころには、固定ページと混ぜずに独立した「ドキュメント置き場」を作れるようになります。

階層型カスタム投稿タイプが向いているケース

WordPress の投稿タイプには、通常の投稿のように親子を持たない非階層型と、固定ページのように親子を持てる階層型があります。階層型にすると、各記事が post_parent(親の投稿ID)と menu_order(並び順)を持てるようになります。

向いているのは、次のような「木構造そのものが情報の意味を持つ」コンテンツです。

  • 製品マニュアル(製品 → 章 → 節)
  • 店舗・施設案内(エリア → 店舗 → フロア)
  • 規約やヘルプセンター(大項目 → 個別の質問)

一方、「一つの記事が複数のグループに属する」「絞り込み検索に使いたい」という場合は、親子関係よりもカスタムタクソノミーのほうが適しています。親は一つしか持てないため、分類の軸が複数あるならタクソノミー、順序つきの目次構造なら階層型投稿タイプ、と考えると判断しやすくなります。

固定ページで同じことをする方法もありますが、会社案内やお問い合わせページと混在すると管理画面の一覧が見づらくなり、権限やテンプレートも分けにくくなります。専用の投稿タイプに切り出す理由はそこにあります。

登録コード:hierarchical と page-attributes がポイント

コードはテーマの functions.php ではなく、独自プラグインか mu-plugin に置くことをおすすめします。投稿タイプの登録をテーマに書くと、テーマを切り替えた時点で管理画面からコンテンツが見えなくなるためです。どうしてもテーマ側に置く場合は子テーマを使います。作業前にはバックアップを取り、ステージング環境で試してから本番に反映してください。

<?php
/**
 * Plugin Name: My Docs Post Type
 * Description: 階層型カスタム投稿タイプ「ドキュメント」を登録します。
 */

add_action( 'init', 'myprefix_register_docs_post_type' );

function myprefix_register_docs_post_type() {
	register_post_type(
		'docs',
		array(
			'labels'       => array(
				'name'          => 'ドキュメント',
				'singular_name' => 'ドキュメント',
				'add_new_item'  => 'ドキュメントを追加',
				'parent_item_colon' => '親ドキュメント:',
			),
			'public'       => true,
			'hierarchical' => true,
			'show_in_rest' => true,
			'has_archive'  => true,
			'menu_icon'    => 'dashicons-book',
			'supports'     => array( 'title', 'editor', 'page-attributes', 'revisions', 'thumbnail' ),
			'rewrite'      => array(
				'slug'       => 'docs',
				'with_front' => false,
			),
		)
	);
}

押さえておきたい設定は三つです。

  • 'hierarchical' => true:親子関係を持てるようにする本体の設定です。
  • 'supports' の page-attributes:編集画面に「親」と「順序」を設定する欄を出すために使います。hierarchical だけを有効にしてもこの欄が出ず、親を選べないという相談はよくあります。
  • 'show_in_rest' => true:ブロックエディターで編集するために必要です。false のままだとクラシックエディターの画面になります。

ブロックエディターでは、親の選択欄はサイドバーの投稿設定内に表示されます。パネルの名前や位置はWordPressのバージョンによって変わることがあり、本記事の説明は執筆時点(2026年10月現在)のものです。見当たらないときはサイドバーの各パネルを開いて確認してください。

親子関係がそのままURLになる

階層型の投稿タイプでは、パーマリンクも親子をたどった形になります。上のコードなら、親「製品A」の下の子「初期設定」は次のようなURLになります。

https://example.com/docs/product-a/setup/

登録直後や rewrite を変更した直後は、書き換えルールを更新しないと404になります。管理画面の「設定」→「パーマリンク」を開いて変更せずに保存すれば更新されます。コード内で毎回 flush_rewrite_rules() を呼ぶと負荷が高いので、使うならプラグインの有効化時だけに限定します。

スラッグの重複にも特徴があります。階層型では同じ親を持つ兄弟同士でだけスラッグの重複が避けられるため、「製品A/初期設定」と「製品B/初期設定」の両方に setup というスラッグを付けられます。逆に、親を付け替えるとURLが変わる点には注意が必要です。公開済みのページの親を変えた場合は、旧URLからのリダイレクトを用意してください。

子ページ一覧・パンくず・目次を表示する

子ページを一覧表示する

親ページのテンプレートで直下の子を並べるには、WP_Query の post_parent を使います。並び順は管理画面で設定した「順序」(menu_order)に合わせます。

<?php
$children = new WP_Query(
	array(
		'post_type'      => 'docs',
		'post_parent'    => get_the_ID(),
		'orderby'        => array( 'menu_order' => 'ASC', 'title' => 'ASC' ),
		'posts_per_page' => 50,
		'no_found_rows'  => true,
	)
);

if ( $children->have_posts() ) :
	echo '<ul class="docs-children">';
	while ( $children->have_posts() ) :
		$children->the_post();
		printf(
			'<li><a href="%s">%s</a></li>',
			esc_url( get_permalink() ),
			esc_html( get_the_title() )
		);
	endwhile;
	echo '</ul>';
	wp_reset_postdata();
endif;

no_found_rows は総件数の計算を省く指定で、ページ送りが不要な一覧なら付けておくとクエリが軽くなります。表示する値は esc_url() と esc_html() でエスケープします。

孫以下まで含めた目次をまとめて出したい場合は、wp_list_pages() に post_type を渡す方法が手軽です。

<?php
$ancestors = get_post_ancestors( get_the_ID() );
$root_id   = $ancestors ? end( $ancestors ) : get_the_ID();

echo '<ul class="docs-toc">';
wp_list_pages(
	array(
		'post_type'   => 'docs',
		'child_of'    => $root_id,
		'title_li'    => '',
		'sort_column' => 'menu_order, post_title',
	)
);
echo '</ul>';

get_post_ancestors() は近い親から順に祖先のIDを配列で返すので、最後の要素が最上位の親になります。これで「今いるマニュアル全体の目次」をどの階層のページでも表示できます。

パンくずリストを作る

祖先の配列を逆順にすれば、最上位からのパンくずになります。

<?php
$crumbs = array_reverse( get_post_ancestors( get_the_ID() ) );

echo '<nav class="docs-breadcrumb" aria-label="パンくずリスト"><ol>';
foreach ( $crumbs as $crumb_id ) {
	printf(
		'<li><a href="%s">%s</a></li>',
		esc_url( get_permalink( $crumb_id ) ),
		esc_html( get_the_title( $crumb_id ) )
	);
}
printf( '<li aria-current="page">%s</li>', esc_html( get_the_title() ) );
echo '</ol></nav>';

SEOプラグインやテーマにパンくず機能がある場合は、そちらがカスタム投稿タイプの親子に対応しているかを先に確認すると、二重表示を避けられます。

テンプレートとアーカイブの扱い

個別ページのテンプレートは、テンプレート階層に従って single-docs.php が使われます(ブロックテーマならサイトエディターで「docs」用の単一テンプレートを作成します)。親ページと子ページで見た目を変えたいときは、テンプレート内で wp_get_post_parent_id() が 0 かどうかで分岐させるのが簡単です。

has_archive を有効にすると /docs/ が一覧ページになりますが、そのままだと子も孫もまとめて並びます。最上位だけを並べたいなら pre_get_posts で絞り込みます。

<?php
add_action( 'pre_get_posts', 'myprefix_docs_archive_top_level_only' );

function myprefix_docs_archive_top_level_only( $query ) {
	if ( is_admin() || ! $query->is_main_query() ) {
		return;
	}
	if ( $query->is_post_type_archive( 'docs' ) ) {
		$query->set( 'post_parent', 0 );
		$query->set( 'orderby', array( 'menu_order' => 'ASC', 'title' => 'ASC' ) );
	}
}

is_admin() と is_main_query() の判定を入れておかないと、管理画面の一覧やウィジェット内のクエリまで変わってしまうので注意してください。

運用でつまずきやすい点

件数が多いと管理画面の一覧が重くなることがあります。 階層型の一覧画面は親子のツリーを組み立てるため、非階層型より多くの投稿を読み込みます。数千件規模になる見込みがあるなら、階層を浅く保つ、製品ごとに投稿タイプやタクソノミーで分ける、といった設計も検討してください。

親を削除したときの子の扱い。 親をゴミ箱に入れても子の post_parent は残るため、子のURLが親の存在を前提にしたまま表示できなくなることがあります。親を整理するときは、先に子の親を付け替えてから作業します。

並び順の管理。 「順序」は数値で、同じ値なら次の並び替え条件に従います。10、20、30 のように間を空けて付けておくと、あとから間に差し込むときに楽です。ドラッグで並べ替えたい場合は並び替え用のプラグインを使う方法もありますが、導入前にカスタム投稿タイプへの対応状況と更新状況を確認してください。

権限を分けたい場合。 ドキュメント担当者だけに編集させたいなら、capability_type と map_meta_cap を指定して専用の権限を作り、ロールに付与します。既存サイトで途中から変えると管理者すら編集できなくなることがあるので、ステージングで確かめてから反映してください。

まとめ

  • 親子構造を持つコンテンツは 'hierarchical' => true の投稿タイプで管理でき、supports に page-attributes を入れると親と順序を設定できる。
  • 登録コードはテーマではなくプラグインか mu-plugin に置き、変更後はパーマリンク設定を保存して書き換えルールを更新する。
  • 子一覧は post_parent、目次は wp_list_pages()、パンくずは get_post_ancestors() で作れる。出力は esc_url()・esc_html() でエスケープする。
  • 親の付け替えや削除はURLに影響するので、リダイレクトと子の付け替えを先に考える。
  • 分類軸が複数あるならタクソノミー、順序つきの目次ならこの方式、と使い分ける。

お気軽にご相談ください

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