ブログ

Blog

WordPress公式のページネーションを理想に近づける

休日の午後、マクドナルドで意味もなくこのブログの過去の記事を読んでいたら、5ページ目からページネーションがひしゃげました。

ボタンが潰れてしまっている

先頭・最終のボタンが表示されたときのモバイルでのチェックをしていませんでした…家に帰ってから、同じ状態になっている mosir のページネーションを改良する作業をしました。

WordPressでページネーションを生成するには paginate_links() 関数を使用します。設定できるパラメータが多いのでカスタマイズの自由度が高いですが、大きな問題が2つあります。それを解決する必要があります。

paginate_links() – Function | Developer.WordPress.org

問題1: 先頭・最終のリンクをカスタマイズできない

ページ数が非常に多いと先頭・最終のページリンクが表示されます。一般的なウェブ制作だと、モバイルではこのボタンをCSSで非表示にしてボタンを減らすのが定番です。

スマートフォンでの理想の状態

ですがWordPressのページネーションは、先頭・最終リンクと通常のページリンクのマークアップが同じです。このため、CSSやJavaScriptで判別して、先頭・最終だけ非表示にすることが難しいです。

paginate_links() のオプション end_size パラメータで、先頭・最終リンクを0=非表示にすればよいのでは?と考えましたが、なんと end_size は「増やす」ことはできても「0」は無視します。先頭と最終のボタンが2個以上あるページネーションなんて、ほとんど見たことないのだが…

echo paginate_links( array(
...
	'end_size' => 0, // 0にしても最低1件表示される
...

) );
修正後のスマートフォンの状態

悩んだ結果、画像のようにしました。

ページネーションの幅を調べ、狭くなったときは、前、次、現在ページ「以外」を非表示にしています。セレクタに使用されているクラス名 .wp-paginate はデフォルトのWordPressにはありません。最後の完成コードで補足します。

.wp-paginate {
...
    container-type: inline-size;
...
}


// If the content width is narrow, the appearance of the pagination changes.
@container ( width <= 40rem ) {

    .wp-paginate .page-numbers:not(.prev):not(.next):not(.current) {
        display: none;
    }

}

問題2: アクセシビリティサポートがない

paginate_links() には aria-label 属性などのアクセシビリティサポートがありません。古くからある関数ですし必須ではないです。しかし、確かにCSSを全部消すとリンクの意味がわかりません。

paginate_links() のドキュメンテーションには、11年も前にそのことを指摘している人がいます。

Improving Accessibility
To add context to the numbered links to ensure that screen reader users understand what the links are for:

アクセシビリティの向上
スクリーンリーダーのユーザーがリンクの意味を理解できるよう、番号付きリンクにコンテキストを追加するには、次の操作を行います。

https://developer.wordpress.org/reference/functions/paginate_links/#comment-419

ボタンの数字テキストに非表示の「ページ」を追加すれば、スクリーンリーダーだけ「ページ 2」になるよ、とのことです。

CSSが反映されていないときの状態

echo paginate_links( array(
...
	'before_page_number' => '<span class="screen-reader-text">' . __( 'Page' ) . ' </span>'
...

) );

しかしここで追加のコメントが入ります。

This should be the default to avoid a lawsuit (Which we’re going through right now so don’t think it wont happen to you!). Additionally you need the previous and next links to say what they are:

訴訟を避けるため、これはデフォルト設定にすべきです(現在訴訟中なので、自分には起こらないとは思わないでください!)。さらに、前のリンクと次のリンクが何であるかを明確にする必要があります。

https://developer.wordpress.org/reference/functions/paginate_links/#comment-419

訴訟とは一体…アクセシビリティガイドラインAAA達成を確約したのに、WordPressで構築したばっかりに達成できなくて訴えられているのか?外国こわいよ!!

それはさておき、前のリンクと次のリンクが何であるかを明確にする必要とあります。サンプルコードによると、右から左に記述する言語への対応を指しています。右から左に記述する言語は「ページ 2」ではなく「2 ページ」だよ、というのです。右から左の言語であるかを判定する is_rtl() 関数を使って、ラベルテキストを改良することにしました。

最終的なコード

最終的に、ページネーションは以下の通りとなりました。長いので get_template_part() で読み込む前提としています。

<?php
/**
 * pager.php
 * Pagination that supports custom query, voice reading and RTL languages.
 *
 * @package mosir
 *
 * @param array $args { query: $wp_query }
 *
 */

global $wp_query;
if( !isset( $args['query'] ) ) {
	$args = array( 'query' => $wp_query );
}

$mosi_max_num_pages = isset( $args['query']->max_num_pages ) ? $args['query']->max_num_pages : (int)0;

// https://developer.wordpress.org/reference/functions/paginate_links/#comment-418
$mosi_pager_big = 999999999; // need an unlikely integer

$mosi_pager_sr_label = '';
$mosi_pager_sr_label .= '<span class="wp-paginate-screen-reader-text">';
$mosi_pager_sr_label .= is_rtl() ? ' ' : '';
$mosi_pager_sr_label .= __( 'Page' );
$mosi_pager_sr_label .= !is_rtl() ? ' ' : '';
$mosi_pager_sr_label .= '</span>';

$mosi_pager_args = array(
	'type'               => 'plain',
	'base'               => str_replace( $mosi_pager_big, '%#%', esc_url( get_pagenum_link( $mosi_pager_big ) ) ),
	'total'              => $mosi_max_num_pages,
	'mid_size'           => 2,
	'end_size'           => 0,
	'prev_next'          => true,
	'prev_text'          => '<span class="wp-paginate-label">' . __( '&laquo; Previous' ) . '</span>',
	'next_text'          => '<span class="wp-paginate-label">' . __( 'Next &raquo;' ) . '</span>',
	'before_page_number' => $mosi_pager_sr_label,
);

if( is_rtl() ) {
	$mosi_pager_args['before_page_number'] = '';
	$mosi_pager_args['after_page_number'] = $mosi_pager_sr_label;
}

if( $mosi_max_num_pages > 1 ) {
	echo '<nav class="wp-paginate">';
	echo paginate_links( $mosi_pager_args );
	echo '</nav>';
}

カスタムクエリの継承

このページネーションは、WP_Queryで生成したカスタムクエリの分割もできるように作成しています。カスタムクエリで使うときは以下のように get_template_part() 関数の第三引数にクエリを渡します。第三引数が存在しなかったら本来のグローバル変数 $wp_query でページ分割をします。

ただ、ページネーションで必要なのは「総ページ数」だけなので、クエリを丸ごと渡す必要はなかったかもしれません。

<?php
$my_custom_args = array(
    'post_type'  => 'myposttype',
    'posts_per_page'  => 5,
    'paged' => (get_query_var('paged')) ? get_query_var('paged') : 1,
    'orderby' => 'date',
    'order' => 'DESC'
);
$my_custom_query = new WP_Query( $my_custom_args );
?>


...

<?php get_template_part( 'template-parts/pager', '', array( 'query' => $my_custom_query ) ); ?>

音声読み上げ用のテキスト

冗長になってしまいましたが、言語判定をして区切りの半角スペースを前に置くか後ろに置くか決めています。また、is_rtl() が真であれば前ではなく後ろに「ページ」を配置しています。

$mosi_pager_sr_label = '';
$mosi_pager_sr_label .= '<span class="wp-paginate-screen-reader-text">';
$mosi_pager_sr_label .= is_rtl() ? ' ' : '';
$mosi_pager_sr_label .= __( 'Page' );
$mosi_pager_sr_label .= !is_rtl() ? ' ' : '';
$mosi_pager_sr_label .= '</span>';

...

if( is_rtl() ) {
	$mosi_pager_args['before_page_number'] = '';
	$mosi_pager_args['after_page_number'] = $mosi_pager_sr_label;
}

確実にマークアップする

paginate_links() 関数だけ実行すると、リンクか単純なリストだけが出力されます。それだとコーディングしづらいです。ページネーションが確実に表示される(2ページ以上ある)場合に nav.wp-paginate で全体を囲んで出力しています。

if( $mosi_max_num_pages > 1 ) {
	echo '<nav class="wp-paginate">';
	echo paginate_links( $mosi_pager_args );
	echo '</nav>';
}

正直しんどい(三回目)

というわけでページネーションを完成させました。全体に改良できてよかったです。もしかするとカスタムクエリの継承は公式テーマではNGで、修正を求められるかもしれませんが。

今日の mosir の開発は、他にもヘッダを完成させました。レイアウトパターンに「small」を追加しています。いわゆるナビゲーションバータイプで、快適に使用できる限界まで高さを詰めています。テーマのドキュメントにはこれを使いたいです。

個人的に、カスタマイザーは一番好きな機能です。ブロックテーマの登場で影が薄くなりましたが、操作も軽快ですし、ブロックテーマのような破壊的な操作はできず、事故も起きにくいです。

公式テーマ作りは正直しんどいですが、毎日少しずつ完成に近付いているのは楽しいです。CSSの命名規則と新着記事の機能をどうしたものか…