WordPress アーカイブ管理を効率化する ― アーカイブルーター導入のメリット

カスタム投稿タイプ(CPT)が増えると、archive-xxx.php が雪だるま式に増え、管理が煩雑になりがちです。アーカイブルーターを導入すると、入口は常に archive.php の1枚だけに集約し、投稿タイプごとに別置きテンプレートへ安全に振り分けられます。この記事では、require_once を1か所(functions.php)にのみ書く」運用に整理した、コピペで使える完全版コードを提示します。
投稿タイプ名とテンプレート名はダミー(例:cpt_news, cpt_event, cpt_book)を使用しています。必要に応じて差し替えてください。


導入の狙いとメリット

  • ファイルの整理archive-xxx.php の乱立を防ぎ、archive.php を1枚に集約。専用テンプレは templates/archive/ にまとめる。
  • 拡張が容易:新規CPT追加時は「ルーターマップに1行追加+テンプレ1枚」だけ。
  • 安全なフォールバック:マップ未定義やテンプレ欠落時は archive.php 内の標準ループをそのまま実行。
  • テーマ階層に非依存locate_template() により子テーマ優先・親テーマfallbackで安全に解決。

ディレクトリ構成(例)

my-theme/
├─ functions.php                       ← ここで router を一度だけ require_once
├─ archive.php                         ← 入口は常にこれ1枚(require は書かない)
├─ functions/
│   └─ archive-router.php              ← ルーター本体(マッピング定義)
└─ templates/
    └─ archive/
        ├─ news.php                    ← cpt_news 用ダミーテンプレ
        ├─ event.php                   ← cpt_event 用ダミーテンプレ
        └─ book.php                    ← cpt_book 用ダミーテンプレ

1) functions.php(ここで一度だけ読み込む)

<?php
/**
 * functions.php
 *
 * テーマ起動時に一度だけルーター本体を読み込む。
 * ※ archive.php には require_once を書かない運用に統一。
 */

// 他の require があるならそのまま…

require_once get_theme_file_path('functions/archive-router.php');

// 以降、my_get_archive_template_for_query() / my_include_archive_template() を全体で利用可能。

2) ルーター本体(functions/archive-router.php)

<?php
/**
 * Archive Router (アーカイブルーター)
 *
 * 目的:
 *  - archive.php を 1 枚に集約しつつ、投稿タイプ(CPT)ごとに別置きテンプレへルーティング。
 *  - マッピング不一致時は null を返し、archive.php 側の標準ループを実行(安全なフォールバック)。
 *
 * 設置例:
 *   /wp-content/themes/my-theme/functions/archive-router.php
 *
 * 読み込み(必ず 1 回だけ):
 *   functions.php で:
 *     require_once get_theme_file_path('functions/archive-router.php');
 */

if (!function_exists('my_get_archive_template_for_query')) {
    /**
     * 現在のクエリに対応するテンプレート(テーマ相対パス)を返す。
     * 見つからなければ null(=archive.php 内のデフォルト表示へ)。
     *
     * 優先:
     *  1) 投稿タイプアーカイブの post_type
     *  2) タクソノミーアーカイブの object_type 先頭
     *
     * @return string|null
     */
    function my_get_archive_template_for_query(): ?string
    {
        // --- 1) 投稿タイプを特定 ---
        $post_type = get_query_var('post_type');

        // is_post_type_archive() の時に配列になる可能性を吸収
        if (is_array($post_type)) {
            $post_type = $post_type[0] ?? '';
        }

        // 念のためクエリ済みオブジェクトからも補完
        if (!$post_type && is_post_type_archive()) {
            $qo = get_queried_object();
            if (!empty($qo->name)) {
                $post_type = $qo->name;
            }
        }

        // --- 2) タクソノミーアーカイブ: 紐づく投稿タイプを推定 ---
        if (!$post_type && is_tax()) {
            $tax_obj = get_queried_object();
            if ($tax_obj && !empty($tax_obj->taxonomy)) {
                $tax = get_taxonomy($tax_obj->taxonomy);
                if ($tax && !empty($tax->object_type) && is_array($tax->object_type)) {
                    // 複数紐づく場合は先頭を採用(必要なら filter で上書き)
                    $post_type = $tax->object_type[0];
                }
            }
        }

        // --- 3) 投稿タイプ → テンプレートのマップ ---
        // ここを編集/追加するだけで拡張可能。ダミー名を使用しています。
        $map = [
            'cpt_news'  => 'templates/archive/news.php',
            'cpt_event' => 'templates/archive/event.php',
            'cpt_book'  => 'templates/archive/book.php',
        ];

        /**
         * 外部からマップを追加・上書き可能にするフック。
         * 例:
         * add_filter('my/archive_template_map', function($map){
         *     $map['cpt_movie'] = 'templates/archive/movie.php';
         *     return $map;
         * });
         */
        $map = apply_filters('my/archive_template_map', $map);

        // --- 4) 解決 ---
        if ($post_type && isset($map[$post_type])) {
            return $map[$post_type];
        }

        // 見つからない → null(archive.php の標準表示へ)
        return null;
    }
}

if (!function_exists('my_include_archive_template')) {
    /**
     * 相対パスで指定されたテンプレートを読み込む。
     * 子テーマ → 親テーマの順で探索。
     *
     * @param string $relative_path テーマ相対パス
     * @return bool 読み込めたら true / 見つからなければ false
     */
    function my_include_archive_template(string $relative_path): bool
    {
        // locate_template は子→親の順に探索する
        $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;
        }

        // 見つからない場合は false を返す(archive.php 側でフォールバック)
        return false;
    }
}

3) archive.php(ルーティング+フォールバック)

<?php
/**
 * archive.php
 *
 * 役割:
 *  1) 投稿タイプに対応する別置きテンプレートを解決し、存在すればそれを読み込んで終了。
 *  2) 未定義・不在なら、このファイル内の標準ループ(デフォルト表示)を実行。
 *
 * 注意:
 *  - ルーターの require_once は functions.php で一度だけ行う前提。
 *  - ここには require_once を書かない(冗長さ・重複を避ける)。
 */

// ルーティング: 見つかれば専用テンプレで完結
if (function_exists('my_get_archive_template_for_query') && function_exists('my_include_archive_template')) {
    if ($template_rel = my_get_archive_template_for_query()) {
        if (my_include_archive_template($template_rel)) {
            return; // ここで処理終了(下のデフォルト描画は実行しない)
        }
    }
}

// --- ここからデフォルト表示(= マップ未定義/テンプレ欠落時のフォールバック) ---

get_header();
?>

<main class="l-container">
  <div class="l-content">
    <header class="c-archive-header">
      <h1 class="c-archive-title">
        <?php
        if (is_post_type_archive()) {
            post_type_archive_title(); // CPT名など
        } elseif (is_category() || is_tag() || is_tax()) {
            single_term_title();       // ターム名
        } else {
            echo esc_html__('Archive', 'my-textdomain');
        }
        ?>
      </h1>

      <?php if (term_description()) : ?>
        <div class="c-archive-desc"><?php echo wp_kses_post(term_description()); ?></div>
      <?php endif; ?>
    </header>

    <?php if (have_posts()) : ?>
      <ul class="c-archive-list">
        <?php while (have_posts()) : the_post(); ?>
          <li class="c-archive-item">
            <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
            <time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
              <?php echo esc_html(get_the_date()); ?>
            </time>
          </li>
        <?php endwhile; ?>
      </ul>

      <nav class="c-archive-pager">
        <?php the_posts_pagination([
          'prev_text' => '&laquo;',
          'next_text' => '&raquo;',
        ]); ?>
      </nav>
    <?php else : ?>
      <p>投稿はありません。</p>
    <?php endif; ?>
  </div>
</main>

<?php get_footer(); ?>

4) ダミーの専用テンプレ例(templates/archive/news.php)

実運用ではデザインやループ構造を自由にカスタマイズしてください。
下記は cpt_news の例。event.phpbook.php も同様に複製して流用できます。

<?php
/**
 * templates/archive/news.php
 *
 * 投稿タイプ: cpt_news 用のアーカイブテンプレート(ダミー)。
 * デザイン/メタ/ソートなどは自由に作り込む。
 */

get_header();

// クエリに設定された投稿タイプ(配列吸収)
$post_type = get_query_var('post_type');
if (is_array($post_type)) { $post_type = $post_type[0] ?? ''; }

// 表示タイトル: 投稿タイプラベルが取れればそれを使う
$cpt_obj = $post_type ? get_post_type_object($post_type) : null;
$title   = ($cpt_obj && !empty($cpt_obj->labels->name)) ? $cpt_obj->labels->name : 'News';
?>

<main class="l-container">
  <div class="l-content">
    <header class="c-archive-header">
      <h1 class="c-archive-title"><?php echo esc_html($title); ?></h1>
      <?php if (is_tax()) : ?>
        <p class="c-archive-tax">Filter: <?php echo esc_html(single_term_title('', false)); ?></p>
      <?php endif; ?>
    </header>

    <?php if (have_posts()) : ?>
      <div class="c-card-grid">
        <?php while (have_posts()) : the_post(); ?>
          <article class="c-card">
            <a href="<?php the_permalink(); ?>" class="c-card__link">
              <h2 class="c-card__title"><?php the_title(); ?></h2>
              <div class="c-card__meta">
                <time datetime="<?php echo esc_attr(get_the_date('c')); ?>">
                  <?php echo esc_html(get_the_date()); ?>
                </time>
                <?php
                // 例: カスタムフィールドやタクソノミー表示を追加
                // $source = get_post_meta(get_the_ID(), 'news_source', true);
                // if ($source) echo '<span class="c-card__badge">'.esc_html($source).'</span>';
                ?>
              </div>
              <p class="c-card__excerpt"><?php echo esc_html(wp_trim_words(get_the_excerpt(), 40)); ?></p>
            </a>
          </article>
        <?php endwhile; ?>
      </div>

      <nav class="c-archive-pager">
        <?php the_posts_pagination([
          'prev_text' => '&laquo;',
          'next_text' => '&raquo;',
        ]); ?>
      </nav>
    <?php else : ?>
      <p>該当する記事はありません。</p>
    <?php endif; ?>
  </div>
</main>

<?php get_footer(); ?>

5) ルーティング・マップの外部拡張(任意)

複数のプラグインや子テーマからマップを安全に足し込むなら、フックを使います。

<?php
// 例: 子テーマの functions.php で
add_filter('my/archive_template_map', function(array $map): array {
    // 追加: 新しい投稿タイプ cpt_movie を別テンプレへ
    $map['cpt_movie'] = 'templates/archive/movie.php';

    // 既存上書き: cpt_news のテンプレ先を差し替え
    // $map['cpt_news'] = 'templates/archive/news-alt.php';

    return $map;
});

導入手順(最短)

  1. functions/archive-router.php を作成。
  2. functions.php一度だけ require_once get_theme_file_path('functions/archive-router.php'); を記述。
  3. archive.php を本記事の内容に差し替える(※ここには require_once を書かない)。
  4. templates/archive/ に、CPTごとのテンプレ(必要分のみ)を作成。
  5. ルーターマップに対象の投稿タイプを追加。

運用のコツ

  • 最初はフォールバックでOK:専用テンプレが不要なら $map に登録せず、archive.php の標準ループに任せる。必要になった時点でテンプレとマップを追加。
  • 命名と置き場所を統一templates/archive/<post-type>.php に揃えると検索と保守が楽。
  • 子テーマ対応locate_template() で子テーマが優先されるため、親テーマを変更せず差し替え可能。

コメント

タイトルとURLをコピーしました