WordPress のマルチサイトで、複数のサイトの投稿を取得して一覧として表示したい場合があります。
このWillさんいんのサイトも然りで、10年前に WP Over Network というプラグインを使って実現したのですが、このプラグイン、既に12年もの間更新されていないのですね。
現在サイトのリニューアルを進めていますが、さすがにもうこのプラグインは使えないので、プラグインを使わないで実装することにしました。

今は ChatGPT というとても便利なツールがありますので、投稿をまとめて表示することは簡単に実装できたのですが、ページネーションでつまづきました。

実現したい条件は以下のとおり。

  • メインサイト(blog_id=1)とサブサイトが3つ(blog_id=2〜4)
  • メインサイトには全てのサイトの投稿をまとめて新着順に表示
  • サブサイトには自サイトとメインサイトの投稿をまとめて表示
  • 各サイトのフロントページでは、最新の n 件を取得
  • アーカイブページでは全件を取得してページネーション処理

各サイトの投稿を取得して表示する仕組みは、switch_to_blog() を使って他サイトの投稿を取得し、複数サイトの投稿をまとめて1つの配列にしてから、投稿日でソートして出力するというものです。

フロントページは問題なく出力できたのですが、アーカイブページでページネーションがきちんと動作せず、2ページ目以降が404エラーになってしまいます。
マルチサイトでなくても、2ページ目以降が404エラーになるという現象は過去に何度か経験がありました(最近はほとんどありませんが)。よくある解決方法としては、パーマリンク設定を再保存する、1ページに表示する最大投稿数を「1」にするなどありましたが、そもそも条件が異なるので今回はそれらも効きません。

ChatGPT に何度も質問し、そのたびに修正したコードと共に「これで正しく動作します」と答えが返ってくるのですが一向に解決せず、無料枠の制限に達するまで何度もやりとりした結果、最終的に「固定ページ」とすることで解決しました。

通常、投稿一覧は固定ページで作成して表示設定の「投稿ページ」に指定して home.php で表示するか、純粋に archive.php で表示するのですが、固定ページで作成したものを「投稿ページ」とせず、page-(slug).php といったテンプレートで表示するということです。
この方式により、無理な rewrite rule や redirect フックが不要で確実に動作するとのことです。

最終的に導き出されたコードは以下です。

functions.php

function get_multisite_posts($paged = 1, $posts_per_page = 5, $for_archive = false) {
    global $blog_id;

    // メイン or サブサイトによって取得対象を切り替え
    $target_sites = ($blog_id == 1) ? [1, 2, 3, 4] : [1, $blog_id];
    $all_posts = [];

    foreach ($target_sites as $site_id) {
        switch_to_blog($site_id);
        $posts = get_posts([
            'post_type'      => 'post',
            'post_status'    => 'publish',
            // front-page では全件取得不要、for_archive=true の時だけ全件取得
            'posts_per_page' => $for_archive ? -1 : $posts_per_page,
            'orderby'        => 'date',
            'order'          => 'DESC',
        ]);
        foreach ($posts as $post) {
            $post->source_blog_id = $site_id;
            $all_posts[] = $post;
        }
        restore_current_blog();
    }

    // 投稿日順に並べ替え
    usort($all_posts, fn($a, $b) => strtotime($b->post_date) - strtotime($a->post_date));

    if ($for_archive) {
        // ページネーション処理
        $total_posts = count($all_posts);
        $offset = ($paged - 1) * $posts_per_page;
        $paged_posts = array_slice($all_posts, $offset, $posts_per_page);
        $max_pages = ceil($total_posts / $posts_per_page);
    } else {
        // front-page では最新 N 件だけ
        $paged_posts = array_slice($all_posts, 0, $posts_per_page);
        $max_pages = 1;
    }

    return [
        'posts' => $paged_posts,
        'max_pages' => $max_pages,
    ];
}

front-page.php

<?php
$data = get_multisite_posts(1, 5, false); // 最新5件を取得
$posts = $data['posts'];

if ($posts):
    foreach ($posts as $post):
        switch_to_blog($post->source_blog_id);
        setup_postdata($post);
        (タイトル等)
        restore_current_blog();
    endforeach;
    wp_reset_postdata();
endif;

page-(slug).php

<?php
// ページ番号
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;

// 投稿取得
$data = get_multisite_posts($paged, get_option('posts_per_page'), true);
$posts = $data['posts'];
$max_pages = $data['max_pages'];

if ($posts):
    foreach ($posts as $post):
        switch_to_blog($post->source_blog_id);
        setup_postdata($post);
        (タイトル等)
        restore_current_blog();
    endforeach;
    echo '</ul>';
    wp_reset_postdata();
endif;

// ページネーション
if ($max_pages > 1):
    echo '<nav aria-label="ページネーション">';
    echo paginate_links([
        'base'      => get_pagenum_link(1) . '%_%',
        'format'    => 'page/%#%/',
        'current'   => $paged,
        'total'     => $max_pages,
        'mid_size'  => 5,
        'prev_text' => '前へ',
        'next_text' => '次へ',
    ]);
    echo '</nav>';
endif;

なお、サイト内検索についても同様で、複数サイトの横断検索を実装する場合は、検索結果テンプレートを固定ページとすることでページネーションが正しく動作します。