WordPress で複数のカスタム投稿タイプ(CPT)を登録していると、個別記事ページごとにsingle-cpt_news.php
、single-cpt_event.php
…とファイルが増えていき、テーマが散らかりがちです。
この問題を解決するのが 「シングルルーター」 です。
1 枚の single.php
を入口にして、投稿タイプごとに適切なテンプレートへ振り分ける仕組みを構築します。
課題:CPT ごとに single-xxx.php が乱立
例として以下の投稿タイプを考えます。
cpt_news
… ニュース記事cpt_event
… イベント情報cpt_book
… 書籍紹介
通常の方法では以下のファイルを作成する必要があります。
single-cpt_news.php
single-cpt_event.php
single-cpt_book.php
CPT が増えると同じ数だけ single-xxx.php
が増えてしまい、保守が面倒になります。
解決策:シングルルーター
基本の考え方
- 入口は
single.php
1 枚だけ - 現在表示中の投稿タイプを判定し、事前に定義したマップから専用テンプレートを読み込む
- マップに存在しなければ
single.php
内の標準ループでフォールバック
ディレクトリ構成例
my-theme/
├─ functions.php
├─ single.php ← 入口は常にこれ1枚
├─ functions/
│ └─ single-router.php ← シングルルーター本体
└─ templates/
└─ single/
├─ news.php ← cpt_news 用
├─ event.php ← cpt_event 用
└─ book.php ← cpt_book 用
コード例
1) functions.php
<?php
// シングルルーターを一度だけ読み込む
require_once get_theme_file_path('functions/single-router.php');
2) シングルルーター本体(functions/single-router.php)
<?php
/**
* Single Router
*
* 目的:
* - single.php を 1 枚に集約しつつ、CPT ごとに別置きテンプレへルーティング
* - マッピング未定義時は null を返し、single.php 内の標準ループを実行
*/
if (!function_exists('my_get_single_template_for_query')) {
function my_get_single_template_for_query(): ?string
{
if (!is_singular()) return null;
$post_type = get_post_type(get_queried_object_id());
$map = [
'cpt_news' => 'templates/single/news.php',
'cpt_event' => 'templates/single/event.php',
'cpt_book' => 'templates/single/book.php',
];
$map = apply_filters('my/single_template_map', $map);
return $map[$post_type] ?? null;
}
}
if (!function_exists('my_include_single_template')) {
function my_include_single_template(string $relative_path): bool
{
$full = locate_template($relative_path);
if (!$full) {
$candidate = get_theme_file_path($relative_path);
if (file_exists($candidate)) {
$full = $candidate;
}
}
if ($full && file_exists($full)) {
require $full;
return true;
}
return false;
}
}
3) single.php
<?php
/**
* single.php
*
* 1) 投稿タイプに対応するテンプレートがあれば読み込み
* 2) 見つからなければ標準ループでフォールバック
*/
if ($template_rel = my_get_single_template_for_query()) {
if (my_include_single_template($template_rel)) {
return; // 終了
}
}
get_header();
if (have_posts()) :
while (have_posts()) : the_post(); ?>
<main class="l-container">
<article <?php post_class(); ?>>
<h1><?php the_title(); ?></h1>
<div><?php the_content(); ?></div>
</article>
</main>
<?php endwhile;
endif;
get_footer();
4) 専用テンプレ雛形(templates/single/news.php)
投稿タイプ
cpt_news
用の例。event.php
やbook.php
もこの雛形をコピーして調整してください。
<?php
/**
* templates/single/news.php
*
* 投稿タイプ: cpt_news 用の個別記事テンプレート
*/
get_header();
if (have_posts()) :
while (have_posts()) : the_post(); ?>
<main class="l-container">
<article <?php post_class('c-entry'); ?>>
<header class="c-entry-header">
<h1 class="c-entry-title"><?php the_title(); ?></h1>
<time class="c-entry-date" datetime="<?php echo esc_attr(get_the_date('c')); ?>">
<?php echo esc_html(get_the_date()); ?>
</time>
</header>
<div class="c-entry-content">
<?php the_content(); ?>
</div>
<footer class="c-entry-footer">
<?php
// タグやカスタムフィールドなどを出力する例
the_tags('<ul class="c-tags"><li>','</li><li>','</li></ul>');
?>
</footer>
</article>
</main>
<?php endwhile;
endif;
get_footer();
メリット
- ファイル数を削減
→ CPT が増えてもsingle.php
とマップに1行追加+専用テンプレを置くだけ。 - 拡張性が高い
→add_filter('my/single_template_map', …)
で外部からマップを追加可能。 - 安全なフォールバック
→ 未定義のCPTでもsingle.php
内の標準ループで表示できる。 - 子テーマ対応
→locate_template()
により子テーマのテンプレートが優先される。
まとめ
- 従来は投稿タイプごとに
single-xxx.php
を作成していた - シングルルーターを導入すると、
single.php
1 枚で一元管理できる - 新規CPTは マップに1行追記+専用テンプレを追加 するだけでOK
アーカイブルーターと併用すれば、一覧ページと詳細ページの両方を効率的に管理でき、テーマの保守性が大幅に向上します。
コメント