テクニック

WordPressページネーションを自作する方法|カスタマイズの手順とコード

この記事の対象: 自作テーマ・子テーマを触れるWeb制作者。PHPの基本文法がわかる人

WordPressのページ送りは、テーマ標準のものをそのまま出すと「1 2 3 … 10」の素っ気ない並びになります。デザインカンプでは丸いボタンが並んでいて、現在ページだけ色が反転していて、両端に「前へ」「次へ」が付いている。この差を埋めるために毎回プラグインを入れるのは重すぎます。

paginate_links() は出力を配列で受け取れるので、マークアップを1から組み直せます。素のPHPとCSSだけで、番号付きも「前へ/次へ」だけの簡易版も作れます。この記事では、そのままコピーして動くコードと、置き場所、書き換える箇所を全部書きます。カスタム投稿タイプや検索結果で動かないときの原因も後半にまとめました。


前提と環境

前提と環境

コードを貼る前に、動作条件をはっきりさせておきます。

項目 前提
テーマ形式 クラシックテーマindex.php / archive.php などのPHPテンプレートを持つもの)
編集対象 自作テーマ、または配布テーマの子テーマ
WordPress 6.x 系。paginate_links() はコアに古くからある関数
PHP 7.4 以上(?? 演算子と短縮配列記法を使用)
必要な権限 テーマファイルの編集権限(FTP/SSH/管理画面のテーマファイルエディター)
追加プラグイン 不要

ブロックテーマ(templates/ ディレクトリにHTMLファイルを置く形式)の場合、投稿一覧は「クエリーループ」ブロックが担当します。ページ送りは「ページ番号」ブロックが出すので、PHPテンプレートを書き換える出番がありません。ブロックテーマでマークアップを変えたいときは、render_block フィルターで出力を差し替えるか、block.json を持つカスタムブロックを作る流れになります。この記事で扱うのはクラシックテーマの方です。

配布テーマを使っている場合は、必ず子テーマを作ってください。親テーマを直接編集すると、テーマ更新のたびに書いたコードが消えます。


paginate_links() の基本を押さえる

paginate_links() の基本を押さえる

まず1行で出してみる

archive.phpindex.php の、投稿ループ(while ( have_posts() ) の閉じ endwhile;)の直後に置きます。

<?php echo paginate_links(); ?>

これだけで <a class='page-numbers'>1</a> のようなリンクが並びます。ただし出力は <a><span> がベタで並ぶだけで、囲みの要素がありません。CSSを当てにくいので、実務ではまず引数を渡して整えます。

よく使う引数

<?php
echo paginate_links( [
    'total'     => $wp_query->max_num_pages,
    'current'   => max( 1, get_query_var( 'paged' ) ),
    'mid_size'  => 2,
    'end_size'  => 1,
    'prev_text' => '前へ',
    'next_text' => '次へ',
    'type'      => 'list',
] );

各引数の意味を表にします。

引数 役割 既定値
total 全ページ数。メインクエリなら $wp_query->max_num_pages 1
current 現在のページ番号 0
mid_size 現在ページの左右に出す番号の数 2
end_size 先頭と末尾に出す番号の数 1
prev_text / next_text 前後リンクの文言 「« 前へ」「次へ »」
show_all true なら全ページ番号を出す( を使わない) false
prev_next 前後リンクを出すかどうか true
type plain / list / array plain
base / format URLの組み立て方。通常は指定不要 自動

mid_sizeend_size は、ページ数が多いときの表示量を決めます。全10ページで現在5ページ目のとき、mid_size => 2, end_size => 1 なら「1 … 3 4 5 6 7 … 10」になります。mid_size => 1 に下げると「1 … 4 5 6 … 10」と短くなり、スマホで折り返さない幅に収まります。

mid_size と end_size が制御する範囲

mid_size = 2(既定)

  • 1 … 3 4 5 6 7 … 10
  • 番号が最大9個並ぶ
  • PCの一覧向き

mid_size = 1

  • 1 … 4 5 6 … 10
  • 番号が最大7個
  • スマホで折り返さない

同じテンプレートで両方を出し分けたいときは、CSSのメディアクエリで一部の番号を display: none にするより、mid_size を小さめに固定して全画面で同じ並びにするほうが崩れません。番号を隠す方式は、隠した分だけ の位置が不自然になり、キーボード操作でも到達できない項目が残ります。

type の3つの違い

返るもの
plain <a><span> が改行区切りで並んだ文字列
list <ul class="page-numbers"><li>…</li></ul> の文字列
array リンク1つずつが要素になった配列

plainlist は文字列なので、囲みのタグを変えたければ正規表現で削るしかなくなります。マークアップを自由にしたいなら array です。次の章で使います。


番号付きページネーションを自作する

番号付きページネーションを自作する

1. 出力用の関数を functions.php に書く

type => 'array' で受け取り、自分でHTMLを組み立てます。テーマの functions.php(子テーマなら子テーマ側)の末尾に追記します。

ファイル: wp-content/themes/あなたのテーマ/functions.php

/**
 * 番号付きページネーションを出力する
 *
 * @param WP_Query|null $query 対象クエリ。省略時はメインクエリ
 * @param array         $args  paginate_links() に渡す追加引数
 */
function myth_pagination( $query = null, $args = [] ) {
    global $wp_query;
    $query = $query instanceof WP_Query ? $query : $wp_query;

    $total = (int) $query->max_num_pages;
    if ( $total < 2 ) {
        return;
    }

    // メインクエリは paged、サブクエリのうち固定ページ上のものは page を見る
    $current = max( 1, (int) get_query_var( 'paged' ) );
    if ( ! $query->is_main_query() && is_page() ) {
        $current = max( 1, (int) get_query_var( 'page' ) );
    }

    $defaults = [
        'total'     => $total,
        'current'   => $current,
        'mid_size'  => 1,
        'end_size'  => 1,
        'prev_text' => '前へ',
        'next_text' => '次へ',
        'type'      => 'array',
    ];
    $links = paginate_links( wp_parse_args( $args, $defaults ) );

    if ( empty( $links ) ) {
        return;
    }
    ?>
    <nav class="myth-pager" aria-label="ページ送り">
        <ul class="myth-pager__list">
            <?php foreach ( $links as $link ) : ?>
                <li class="myth-pager__item"><?php echo wp_kses_post( $link ); ?></li>
            <?php endforeach; ?>
        </ul>
    </nav>
    <?php
}

書き換える箇所は3つです。

  • myth_pagination — 関数名の接頭辞。他のテーマやプラグインと衝突しないよう、自分のプロジェクト固有の3〜5文字に変える
  • myth-pager — CSSクラスの接頭辞。関数名と揃えておく
  • mid_size / prev_text / next_text — 表示件数と文言

$total < 2 で早期リターンしているのは、1ページしかないときに空の <nav> を出さないためです。空要素は余白だけ残るので、CSSで :empty を書くより関数側で止めるほうが確実です。

wp_kses_post() を通しているのは、paginate_links() の戻り値がフィルター(paginate_links フック)を経由するためです。他のプラグインが値を書き換える可能性がある以上、出力時にエスケープしておきます。

第2引数の $args を用意してあるので、呼び出し側で myth_pagination( null, [ 'mid_size' => 3 ] ) のように上書きできます。アーカイブごとに表示量を変えたいとき、関数を複製せずに済みます。

2. テンプレートから呼ぶ

ファイル: wp-content/themes/あなたのテーマ/archive.phpindex.phpsearch.phphome.php も同じ)

<?php if ( have_posts() ) : ?>
    <div class="post-list">
        <?php while ( have_posts() ) : the_post(); ?>
            <article class="post-list__item">
                <a href="<?php the_permalink(); ?>">
                    <h2><?php the_title(); ?></h2>
                </a>
            </article>
        <?php endwhile; ?>
    </div>

    <?php myth_pagination(); ?>

<?php else : ?>
    <p>記事が見つかりませんでした。</p>
<?php endif; ?>

endwhile; の後、else: の前です。ここを間違えて while の内側に置くと、記事の数だけページ送りが出ます。

3. CSSを当てる

paginate_links() が付けるクラスは決まっています。これを覚えておくと、どのテーマでも同じCSSが使い回せます。

要素 クラス
通常のページ番号リンク a.page-numbers
現在のページ span.page-numbers.current
省略記号(…) span.page-numbers.dots
前へ a.prev.page-numbers
次へ a.next.page-numbers

ファイル: wp-content/themes/あなたのテーマ/style.css(または読み込んでいるCSSファイル)

.myth-pager {
  margin: 3rem 0;
}

.myth-pager__list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.myth-pager__item {
  margin: 0;
}

.myth-pager .page-numbers {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  height: 2.75rem;
  padding: 0 0.75rem;
  border: 1px solid #d8d8d8;
  border-radius: 999px;
  color: #333;
  font-size: 0.9375rem;
  line-height: 1;
  text-decoration: none;
  transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}

.myth-pager a.page-numbers:hover {
  background-color: #f4f4f4;
  border-color: #b5b5b5;
}

.myth-pager .page-numbers.current {
  background-color: #1a1a1a;
  border-color: #1a1a1a;
  color: #fff;
  font-weight: 600;
}

.myth-pager .page-numbers.dots {
  border-color: transparent;
  min-width: 1.5rem;
  padding: 0;
}

.myth-pager .prev,
.myth-pager .next {
  padding: 0 1.125rem;
}

/* キーボード操作時のフォーカスを消さない */
.myth-pager a.page-numbers:focus-visible {
  outline: 2px solid #1a1a1a;
  outline-offset: 2px;
}

書き換える箇所は色とサイズです。

  • #1a1a1a — 現在ページの背景色。サイトのキーカラーに差し替える
  • #d8d8d8 / #f4f4f4 — 枠線とホバー背景。トーンに合わせる
  • min-width: 2.75rem / height: 2.75rem — ボタンの大きさ。44px相当のタップ領域を確保しているので、これ以上小さくするならモバイルの操作性を確認する
  • border-radius: 999px — 角丸。四角にするなら 4px などに

.page-numbers.dots の枠線を透明にしているのは、「…」がボタンに見えてクリックされるのを防ぐためです。押せないものを押せそうに見せない、という判断です。

現在ページの背景をキーカラーに差し替えるときは、文字色とのコントラスト比を確認してください。淡い色を背景にして文字を白のままにすると、番号が読めなくなります。背景が明るいなら文字色を #1a1a1a 側に戻すほうが安全です。

:focus-visible は主要ブラウザの現行版でひととおり使えます。古い環境をサポート対象に含めるなら :focus も併記してください。


「前へ/次へ」だけの簡易版

「前へ/次へ」だけの簡易版

記事詳細に近い一覧や、ページ数が読めない検索結果では、番号を出さず前後だけにするほうがすっきりします。WordPressには専用の関数が用意されているので、paginate_links() を使わずに済みます。

アーカイブ・一覧ページの前後リンク

ファイル: wp-content/themes/あなたのテーマ/functions.php

/**
 * 「前へ/次へ」だけの簡易ページ送り
 */
function myth_simple_pager() {
    global $wp_query;
    if ( (int) $wp_query->max_num_pages < 2 ) {
        return;
    }

    $prev = get_previous_posts_link( '新しい記事' );  // ページ番号が小さい方向
    $next = get_next_posts_link( '過去の記事' );      // ページ番号が大きい方向
    ?>
    <nav class="myth-simple-pager" aria-label="ページ送り">
        <div class="myth-simple-pager__side myth-simple-pager__side--prev">
            <?php echo $prev ? wp_kses_post( $prev ) : ''; ?>
        </div>
        <div class="myth-simple-pager__side myth-simple-pager__side--next">
            <?php echo $next ? wp_kses_post( $next ) : ''; ?>
        </div>
    </nav>
    <?php
}

呼び出しは番号版と同じ位置です。

<?php myth_simple_pager(); ?>

ここでよく混乱するのが、nextprevious の向きです。

関数 進む方向 表示される内容
get_next_posts_link() ページ番号が増える より古い記事
get_previous_posts_link() ページ番号が減る より新しい記事

日付降順(既定)の一覧では「次のページ=古い記事」になります。文言を「次へ」にすると読者は迷いませんが、「新しい記事へ」と書くつもりで get_next_posts_link() を使うと逆になります。上のコードで 新しい記事previous 側に置いているのはそのためです。

なお get_next_posts_link() はメインクエリの max_num_pages を見ます。サブループの前後リンクを出したいときは、第2引数に総ページ数を渡してください(get_next_posts_link( '過去の記事', $news_query->max_num_pages ))。渡さないと最終ページでも「過去の記事」が出続けます。

CSSで両端に寄せる

.myth-simple-pager {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin: 3rem 0;
}

.myth-simple-pager__side {
  flex: 1;
}

.myth-simple-pager__side--next {
  text-align: right;
}

.myth-simple-pager a {
  display: inline-block;
  padding: 0.75rem 1.5rem;
  border: 1px solid #d8d8d8;
  border-radius: 999px;
  color: #333;
  font-size: 0.9375rem;
  text-decoration: none;
}

.myth-simple-pager a:hover {
  background-color: #f4f4f4;
}

__sideflex: 1 を持たせているので、片方のリンクが無くても(1ページ目には「新しい記事」が出ない)、もう片方は右端に固定されます。空の <div> が場所取りをする形です。justify-content: space-between だけに頼ると、要素が1つのときに左に寄ってしまいます。

投稿本文の分割ページ

<!--nextpage--> で記事を分割している場合は、上とは別の関数です。single.phpthe_content() の直後に置きます。

<?php
wp_link_pages( [
    'before'      => '<nav class="myth-link-pages" aria-label="この記事のページ"><span>ページ:</span>',
    'after'       => '</nav>',
    'link_before' => '<span>',
    'link_after'  => '</span>',
] );

一覧のページ送りと本文の分割は別の仕組みなので、混ぜて考えると原因追跡が難しくなります。「記事一覧の2ページ目」は paged、「1本の記事の2ページ目」は page が動く、という対応で覚えておくと切り分けが速くなります。


カスタム投稿タイプとサブループの注意点

カスタム投稿タイプとサブループの注意点

ここが一番つまずきます。原因はほぼ2つに絞れます。

ページ送りが動かないときの分岐
クエリの種類を確認メインクエリかWP_Queryか
paged の取得先paged か page かを判定
total の参照先$wp_query か自作クエリか
URL構造を確認パーマリンク再保存とrewrite

上から順に見ると、たいていの「404になる」「2ページ目も同じ記事」はここで止まります。

WP_Query で自作したループ

new WP_Query() で組んだループにページ送りを付けるなら、paged を明示的に渡す必要があります。渡さないと、何ページ目を開いても常に1ページ目の内容が返ります。

ファイル: wp-content/themes/あなたのテーマ/archive-news.php など

<?php
$paged = max( 1, (int) get_query_var( 'paged' ) );

$news_query = new WP_Query( [
    'post_type'      => 'news',   // ← 自分の投稿タイプ名に変更
    'posts_per_page' => 12,       // ← 1ページの件数
    'paged'          => $paged,
    'post_status'    => 'publish',
] );

if ( $news_query->have_posts() ) : ?>
    <div class="post-list">
        <?php while ( $news_query->have_posts() ) : $news_query->the_post(); ?>
            <article class="post-list__item">
                <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
            </article>
        <?php endwhile; ?>
    </div>

    <?php myth_pagination( $news_query ); ?>

<?php
    wp_reset_postdata();
endif;

ポイントは3つです。

  • 'paged' => $paged を必ず渡す
  • ページ送り関数に $news_query を引数で渡す$wp_query->max_num_pages を見ると総ページ数がズレる)
  • ループの後に wp_reset_postdata() を呼ぶ。これを忘れると、以降の the_title() などが最後の記事のまま固定される

先ほどの myth_pagination() は第1引数で WP_Query を受け取れるように作ってあるので、そのまま渡せます。

なお、カスタム投稿タイプのアーカイブを出すだけなら、archive-news.phpWP_Query を新しく作る必要はありません。件数だけ変えたいのであれば後述の pre_get_posts で調整し、テンプレートはメインループをそのまま回すほうが、ページ送りもURL生成も自動で噛み合います。WP_Query を書くのは、複数の投稿タイプを混ぜる・独自の並び順にする・1画面に複数の一覧を出すといった、メインクエリでは表現できないときだけにしてください。

固定ページ上のサブループは page を使う

固定ページ(page.phppage-news.php)の中でサブループを回す場合、URLの /page/2/paged ではなく page に入ります。固定ページ自体が「投稿の分割ページ」を持てる仕様のためです。

// 固定ページテンプレートの中では page を見る
$paged = max( 1, (int) get_query_var( 'page' ) );

両方に対応させたいなら、こう書きます。

$paged = (int) get_query_var( 'paged' );
if ( ! $paged ) {
    $paged = (int) get_query_var( 'page' );
}
$paged = max( 1, $paged );

myth_pagination() の中で is_page() を見て分岐しているのは、この差を吸収するためです。ただしURL生成側も影響を受けるので、固定ページ上のページ送りが /page/2/ で404になる場合は次項も確認してください。

固定ページの2ページ目が404になるとき

固定ページと同じスラッグのカスタム投稿タイプ・カテゴリーが存在すると、/page/2/ の解釈が競合して404になることがあります。回避策は baseformat を明示することです。

$base = trailingslashit( get_permalink() ) . user_trailingslashit( 'page/%#%', 'single_paged' );

echo paginate_links( [
    'base'    => $base,
    'format'  => '?paged=%#%',
    'total'   => $news_query->max_num_pages,
    'current' => $paged,
    'type'    => 'array',
] );

%#% がページ番号に置き換わるプレースホルダーです。base はリンク先URLの雛形、format はクエリ文字列側の雛形で、パーマリンクが「基本」設定のときはこちらが使われます。

これでもダメなら、固定ページ側を使うのをやめて、カスタム投稿タイプのアーカイブ(archive-news.php)に寄せるほうが早い場面が多いです。アーカイブ側は WordPress がURL構造を用意してくれるので、余計な調整が要りません。固定ページに一覧を置きたい理由が「URLを /news/ にしたい」だけなら、register_post_type()rewrite.slug で同じURLが作れます。

カスタム投稿タイプの登録設定

register_post_type() の設定次第で、アーカイブそのものが存在しないことがあります。

register_post_type( 'news', [
    'label'         => 'お知らせ',
    'public'        => true,
    'has_archive'   => true,             // ← これが false だと /news/ が開かない
    'rewrite'       => [ 'slug' => 'news', 'with_front' => false ],
    'supports'      => [ 'title', 'editor', 'thumbnail' ],
    'show_in_rest'  => true,
] );

has_archive => true に変えたあと、管理画面の「設定 > パーマリンク」を開いて保存ボタンを押してください。押すだけでリライトルールが再生成されます。これを忘れると、コードは正しいのに404のままになります。ページ送りのトラブルで最も多い原因がこれです。

with_front => false は、パーマリンク設定に接頭辞(/blog/ など)が付いている場合に、その接頭辞をカスタム投稿タイプのURLから外す指定です。/blog/news/ にしたくないなら false にします。

has_archive に文字列を渡すと、アーカイブのURLだけ別名にできます('has_archive' => 'news-list')。ただし投稿の個別URLは rewrite.slug 側で決まるので、この2つを別々に指定すると一覧と詳細でパスが分かれます。運用で混乱しやすいので、理由がなければ揃えてください。

WordPressそのものの運用面については、こちらの記事もあわせてどうぞ。


検索結果ページでページ送りが壊れるとき

検索結果ページでページ送りが壊れるとき

検索結果(search.php)は挙動が少し違います。

2ページ目でクエリが消える

パーマリンク設定によっては、検索結果の2ページ目URLが /search/キーワード/page/2/ になったり ?s=キーワード&paged=2 になったりします。paginate_links() は現在のURLから base を組むので、通常は自動で正しく作られます。壊れるのは、テンプレート側で WP_Query を使って検索結果を作り直しているときです。この場合、base は現在のURLから作られるのに totalcurrent は自作クエリ側の値になるため、リンク先と中身がズレます。

検索はメインクエリに任せるのが正解です。件数だけ変えたいなら pre_get_posts で調整します。

ファイル: wp-content/themes/あなたのテーマ/functions.php

/**
 * 検索結果の表示件数を変える
 */
function myth_search_posts_per_page( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }
    if ( $query->is_search() ) {
        $query->set( 'posts_per_page', 20 );  // ← 件数を変更
    }
}
add_action( 'pre_get_posts', 'myth_search_posts_per_page' );

is_admin()is_main_query() の2つのガードは必須です。片方でも抜けると、管理画面の投稿一覧の件数まで変わったり、サイドバーのウィジェットに影響が出たりします。

同じ形でカスタム投稿タイプのアーカイブ件数も変えられます。

function myth_archive_posts_per_page( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }
    if ( $query->is_post_type_archive( 'news' ) ) {  // ← 投稿タイプ名
        $query->set( 'posts_per_page', 12 );          // ← 件数
    }
    if ( $query->is_category() ) {
        $query->set( 'posts_per_page', 9 );
    }
}
add_action( 'pre_get_posts', 'myth_archive_posts_per_page' );

管理画面の「設定 > 表示設定 > 1ページに表示する最大投稿数」を変える方法もありますが、あれはサイト全体に効きます。アーカイブごとに件数を変えたいなら pre_get_posts のほうが素直です。

件数を変えたら、既存のURLがどうなるかも見てください。1ページ10件を20件に増やすと、それまで存在していた後半のページURLが消えます。運用中のサイトで外部からリンクされている可能性があるなら、件数の変更は公開直後に済ませておくほうが安全です。

検索キーワードが空のとき

?s= だけでキーワードが空だと、WordPressは検索ではなくトップページを返します。検索フォームのバリデーションを入れておくと、2ページ目以降の挙動も安定します。

function myth_redirect_empty_search( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }
    if ( isset( $_GET['s'] ) && '' === trim( wp_unslash( $_GET['s'] ) ) ) {
        $query->set( 's', ' ' );  // 空白1文字にして「該当なし」を返させる
        $query->is_search = true;
        $query->is_home   = false;
    }
}
add_action( 'pre_get_posts', 'myth_redirect_empty_search' );

SEO面で押さえておくこと

SEO面で押さえておくこと

ページ送りを自作すると、標準のテーマが出していた要素が抜けることがあります。

rel=”next” / rel=”prev” は不要

以前は <head>rel="next" / rel="prev" を入れる指定がありましたが、Googleは検索のインデックス用シグナルとしてこれを使っていないと表明しています。ブラウザの先読みヒントとしては意味が残るものの、SEOのために追加する必要はありません。

代わりに気を配るのは次の3つです。

  • 2ページ目以降を noindex にしない。中の記事へのクロール経路が切れます
  • 正規URLは各ページ自身。2ページ目の canonical を1ページ目に向けると、2ページ目の記事が拾われにくくなります
  • ページ送りを JavaScript だけで動かさない<a href> の実URLがあれば、クローラーもユーザーもたどれます

paginate_links() は素直に <a href> を出すので、この3つは自然に満たされます。無限スクロールに置き換えるときだけ、代替のリンクを用意する必要が出てきます。

一覧ページのタイトルも見ておいてください。2ページ目以降も1ページ目と同じ <title> のままだと、検索結果に同じ見出しが並びます。SEOプラグインを入れていれば「2ページ目」といった接尾辞が自動で付くことが多いですが、自作テーマで wp_title 相当を自前で組んでいる場合は付きません。get_query_var( 'paged' ) が2以上のときに「(2ページ目)」を足すだけで十分です。

アクセシビリティの最低限

<nav class="myth-pager" aria-label="ページ送り">

<nav>aria-label を付けるのは、1ページ内に複数の <nav>(グローバルメニューとページ送り)があるときに区別するためです。スクリーンリーダーが「ナビゲーション」を2つ読み上げるとき、どちらがどれか分かるようになります。

現在ページに aria-current="page" を付けたい場合は、paginate_links フィルターで足せます。

/**
 * 現在ページのマークアップに aria-current を付ける
 */
function myth_pager_aria_current( $link ) {
    if ( false !== strpos( $link, 'current' ) ) {
        $link = str_replace( '<span', '<span aria-current="page"', $link );
    }
    return $link;
}
add_filter( 'paginate_links_output', 'myth_pager_aria_current' );

paginate_links_output は比較的新しいフィルターです。使っている WordPress で効かない場合は、myth_pagination()foreach の中で同じ置換をしてください。

あわせて、番号だけのリンクは音声だけで聞くと「2」「3」としか読まれません。prev_text / next_text を記号(« »)だけにしないで文言を入れておくと、前後関係が音声でも伝わります。記号を使いたいときは <span class="screen-reader-text">前のページ</span> を添える形にしてください。

流入の変化を追いたいときは、計測側の準備もセットで考えておくと判断が早くなります。


うまくいかないとき

うまくいかないとき

作業中に詰まりやすい箇所を、症状から引ける形でまとめます。

ページ送りが何も表示されない

確認すること 対処
総記事数が1ページ分に収まっていないか 記事を増やすか posts_per_page を一時的に2などに下げて確認
max_num_pages が1になっていないか var_dump( $wp_query->max_num_pages ); で実値を見る
呼び出し位置が endwhile; の外か while の内側だと記事数ぶん出る/if の外だと変数が空
サブループなのに引数を渡していないか myth_pagination( $news_query ) のように渡す

2ページ目を開いても1ページ目と同じ記事

WP_Querypaged を渡していません。'paged' => max( 1, (int) get_query_var( 'paged' ) ) を配列に足してください。固定ページテンプレートの中なら get_query_var( 'page' ) です。

2ページ目が404になる

順に試します。

  1. 管理画面の「設定 > パーマリンク」を開いて保存。リライトルールが古いままのケースが最も多い
  2. カスタム投稿タイプの has_archivetrue か確認
  3. 固定ページのスラッグと、カスタム投稿タイプ/カテゴリーのスラッグが重複していないか確認
  4. paginate_links()baseformat を明示

.htaccess が書き込み不可になっていると、パーマリンクを保存してもルールが反映されません。サーバーのファイル権限も見てください。Nginx環境ではそもそも .htaccess を読まないので、サーバー側のリライト設定を確認します。

数字だけ出て「前へ/次へ」が出ない

1ページ目には「前へ」が、最終ページには「次へ」が出ません。これは仕様です。中間のページで開いて確認してください。それでも出ないなら、prev_next 引数が false になっていないか見ます(既定は true)。

CSSが効かない

.page-numbers クラスは親テーマや使用中のプラグインも使っている可能性があります。開発者ツールで実際に当たっているルールを確認し、詳細度が足りなければ .myth-pager .page-numbers のように自作の親クラスを前置してください。!important を足す前に、この形で解決できることがほとんどです。

子テーマのCSSが読み込まれていない場合もあります。functions.php で親テーマと子テーマのスタイルを両方 wp_enqueue_style() しているか確認します。

ページ送りの位置がズレる、余白が二重になる

親テーマ側のページネーション用CSSが残っている可能性があります。テーマ標準の the_posts_pagination() を呼んでいる箇所が別にないか、テンプレート全体を検索してください。archive.php を子テーマにコピーして自作関数を足したものの、親テーマの footer.php 側でも出していた、というパターンがあります。

スマホで番号が折り返して2行になる

mid_size を 1 に下げるのが第一手です。それでも収まらないなら、CSSで数字部分だけ小さくします。

@media (max-width: 480px) {
  .myth-pager .page-numbers {
    min-width: 2.5rem;
    height: 2.5rem;
    padding: 0 0.5rem;
    font-size: 0.875rem;
  }
  .myth-pager .prev,
  .myth-pager .next {
    padding: 0 0.875rem;
  }
}

タップ領域は44px(2.75rem)以上が推奨なので、2.5rem に下げるときは要素同士の gap を詰めすぎないようにしてください。誤タップが増えます。

ページ番号のリンク先に不要なパラメータが付く

絞り込みフォームなどで ?tag=xxx を付けている場合、paginate_links() の既定ではそのパラメータが引き継がれません。add_args に渡すと保持できます。

'add_args' => array_map( 'sanitize_text_field', wp_unslash( $_GET ) ),

逆に不要なパラメータまで付いてくるときは、必要なキーだけを配列で組み直してください。$_GET を丸ごと渡すと、計測用のパラメータまでページ送りのURLに残り、同じ内容のURLが増えます。


よくある質問

よくある質問

paginate_links と the_posts_pagination はどう違いますか

the_posts_pagination()paginate_links() を内部で呼び、<nav class="navigation pagination"> で囲んで出力する関数です。手軽ですが囲み要素のマークアップが固定されます。囲みごと自分で組みたい場合は paginate_links()type => 'array' を渡し、配列をループして自分でHTMLを書く形が自由度が高いです。

プラグインを使わずに実装して問題ありませんか

問題ありません。paginate_links() はWordPressのコア関数なので、プラグインの更新停止やバージョン非互換の影響を受けません。読み込むJS/CSSも増えないため表示速度の面でも有利です。プラグインが要るのは、Ajaxでの部分読み込みや無限スクロールなど、標準の仕組みから外れる挙動を足すときです。

カスタム投稿タイプの2ページ目が404になります

まず管理画面の「設定 > パーマリンク」を開いて保存ボタンを押してください。リライトルールが再生成され、これで直るケースが大半です。直らない場合は register_post_type()has_archivetrue になっているか、固定ページと同じスラッグを使っていないかを確認します。

1ページの表示件数をアーカイブごとに変えられますか

pre_get_posts フックで変えられます。is_admin()is_main_query() でガードしたうえで、$query->is_post_type_archive( 'news' ) のような条件分岐を書き、$query->set( 'posts_per_page', 12 ) で件数を指定します。管理画面の表示設定はサイト全体に効くので、個別に変えたいときはこの方法を使います。

ブロックテーマでも同じコードが使えますか

そのままでは使えません。ブロックテーマの投稿一覧はクエリーループブロックが担当し、PHPテンプレートを経由しないためです。マークアップを変えたい場合は render_block フィルターでページ番号ブロックの出力を差し替えるか、カスタムブロックを作る形になります。この記事のコードはクラシックテーマ向けです。

2ページ目以降を noindex にすべきですか

しないでください。2ページ目以降を noindex にすると、そのページからしかたどれない記事へのクロール経路が細くなります。一覧ページ自体が検索結果に出る必要はありませんが、通り道としての価値は残るので、素直にインデックス可能なままにしておくのが無難です。canonical も各ページ自身に向けます。


まとめ

ページ送りの自作でやることは、実質3つに整理できます。

  • type => 'array' で受け取る — 囲みのHTMLを自分で書けるようになる
  • クエリとページ番号の対応を合わせる — サブループなら paged を渡し、総ページ数もそのクエリから取る
  • URL構造を疑う — 404の大半はパーマリンクの再保存で直る

この3つを押さえておけば、番号付きも「前へ/次へ」も、カスタム投稿タイプも同じ考え方で組めます。プラグインを足さずに済む分、納品後の保守も軽くなります。

案件でどのCMSを選ぶか迷っている段階なら、こちらの比較も参考になります。