Ajaxで非同期通信のページネーションをWordPressループ(記事一覧)のカテゴリー絞り込み検索で実装する方法と合わせて徹底解説

みなさんこんにちは!エンジニアの高澤です!
今回はAjaxで非同期通信のページネーションをWordPressループ(記事一覧)のカテゴリー絞り込みで実装する方法と合わせて徹底解説していきたいと思います。
わたくしごとですが、今回の内容は非常に待望のコンテンツでございまして、これまで絞り込み検索でのWordPressループをAjaxで非同期化しつつ、それに対応した非同期のページネーションを実装する、という記事や情報がなかなか見つかりませんでした。
また、特に「Ajaxの非同期のページネーション」の実装は、当時はChatGPTに聞いても実装が難しかったため、今回の記事を公開できることを非常に喜ばしく思います。
今回の内容は、WordPressでAjaxを使った非同期の絞り込み検索機能やAjaxでの非同期のページネーションの実装方法を知りたいエンジニアの方などにお役に立てる内容となっているかと思いますため、よろしければお仕事などでご活用ください。
目次
当記事で作れるもの
まず実装方法の説明の前に、事前に作れるものをご紹介しておきたいと思います。
作れるものとしては下図のようなUIのAjaxによる非同期で動く記事ページの絞り込み検索機能になります。

WordPressデフォルト機能の「カテゴリー」によって絞り込みが可能であり、セレクトボックスから他のカテゴリーを選択したら、ページのローディングが必要なくページの一覧の部分だけが描画が切り替わる形で更新されます。
また、それに対応した非同期のページネーションも表示されます。ページネーションの次の数字をクリックしたときにページのローディング(画面遷移)なしで切り替えることが可能です。
実際の動作としては、以下の動画で確認できます。
ページ遷移が起こらないので、ユーザーはストレスなくWebサイトやWebアプリケーションを操作することが可能です。
今回の記事では、このような実装ができるようになる内容となっております。
ずっとこの情報が欲しかった…
WordPressのAjax(Ajax・非同期通信)とは
Ajaxとは、ページを再読み込みせずにサーバーと非同期通信を行う仕組みのことを言います。
これにより、ボタンをクリックした際の「いいね」処理や、フォーム送信、コメントの読み込みなどをスムーズに行うことができます。
また、WordPressのAjaxとは、WordPressのコア(WordPressそのもの)によって提供されるAjaxのことを言います。
admin-ajax.phpという専用のエンドポイントを通じて、JavaScript(主にjQuery)からPHPの処理を呼び出す形で実装されます。ユーザー体験を向上させるための重要な技術と言っても過言ではありません。
100%初見の方は、ここまでの説明を聞いてよくわからないかと思います。
そのため、当サイトでは以下の記事にてAjaxについて詳しくまとめておりますので、よろしければご活用ください。
非同期(Ajax)で作るメリット・使いどころ
WordPressの絞り込み検索機能を非同期(Ajax)で作るメリット・使いどころについて解説いたします。
繰り返しになりますが、Ajax (非同期処理)を活用すると、ページ全体を再読み込みせずに必要なデータだけをサーバーとやり取りでき、ユーザーにとって快適な操作体験を提供できます。
たとえば、投稿一覧の「もっと見る」ボタン、いいね機能、コメントの読み込み、リアルタイム検索などが代表的な使いどころです。
画面遷移がないため、動作が軽くスムーズで、ユーザーの離脱防止にもつながります。また、開発面では部分的にデータ処理を組み込めるため、柔軟で効率的な設計が可能になります。
WordPressでもadmin-ajax.phpを使うことで、こうした非同期処理を簡単に取り入れることができます。動きのあるUIやレスポンスの速さが求められる場面では、Ajaxは欠かせない技術になります。
非同期(Ajax)のページネーションでの記事一覧の実装方法
それでは早速、非同期(Ajax)のページネーションでの記事一覧の実装方法を解説いたします。
ここから解説する内容に沿って手順通りに進めていただければ無理なく簡単に実装できるので、ぜひ実装のお仕事などの際にご活用いただければと思います。
まず実装の流れとしては、以下になります。
- 投稿からサンプルの記事を複数作成
- カテゴリーからサンプルのカテゴリーを複数作成
- 投稿とカテゴリーを紐付け
- ソースコードで機能を実装
それぞれ解説いたします。
投稿からサンプルの記事を複数作成
コードで機能を実装する前に、そもそも記事ページやカテゴリーなどのデータがデータベースに必要です。そのため、手動でもなんでも良いので、まずはデータを追加しておきましょう。
まずは投稿からサンプルの記事を複数作成します。
WordPress管理画面左メニューの「投稿」→「投稿を追加」などからどんどん適当で問題ないのでページを追加していきます。

もし作成が面倒であれば、よろしければ以下の記事をご活用いただけますと効率的に作業の負担を軽減できるかと思いますのでご活用ください。
作成ができたら、次に進みましょう。
カテゴリーからサンプルのカテゴリーを複数作成
次は、カテゴリーからサンプルのカテゴリーを複数作成します。
WordPress管理画面左メニューの「投稿」→「カテゴリー」からどんどん適当で問題ないのでカテゴリーを追加していきます。

もし作成が面倒であれば、よろしければ以下の記事をご活用いただけますと効率的に作業の負担を軽減できるかと思いますのでご活用ください。
作成ができたら、次に進みましょう。
投稿とカテゴリーを紐付け
WordPress管理画面左メニューの「投稿」→「投稿一覧」などからどんどん適当で問題ないので記事ページとカテゴリーの紐付けをしていきます。
筆者的には「投稿一覧」の各ページの「クイック編集」のリンクテキストをクリックして表示されるクイック編集の設定UIから適当なカテゴリーを選択して保存するやり方が最も時短にもなり簡単なのでおすすめです。

もし作業が面倒であれば、よろしければ以下の記事をご活用いただけますと効率的に作業の負担を軽減できるかと思いますのでご活用ください。
紐付けができたら、次に進みましょう。
ソースコードで機能を実装
ここまでで管理画面でデータを追加したりして実装前のセッティングが完了したかと思いますが、完了したらいよいよソースコードで実装していきます。
ちなみにテーマの機能として実装することを前提にしておりますが、プラグイン開発でも掲載するコードはほぼそのまま使えるので、プラグイン開発をされている方も参考にしていただけたらと思います。
難しい説明は後回しにしておき、まずは実装してしまいましょう。
以下のコードを、絞り込み検索の記事一覧として表示したページや箇所にコピー&ペーストしてください。
例えば、テーマを構成するindex.phpやpage.php、page-{固定ページのスラッグ}.phpなどにペーストする形になるかと思います。
<style>
/* カテゴリーフィルターのスタイル */
.category-filter-wrapper {
max-width: 800px;
margin: 0 auto;
}
.category-filter-container {
margin: 20px 0;
}
.category-filter-controls {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
}
.category-filter-controls label {
font-weight: bold;
color: #333;
margin-right: 10px;
}
.category-filter-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
font-size: 14px;
min-width: 200px;
}
.category-filter-select:focus {
outline: none;
border-color: #007cba;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.1);
}
.results-count {
color: #666;
font-size: 14px;
margin-left: auto;
}
.category-filter-results-wrapper {
min-height: 200px;
}
.category-filter-results {
display: grid;
/* grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); */
gap: 20px;
margin-bottom: 30px;
}
.post-item {
display: flex;
background: white;
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s ease;
}
.post-item:hover {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.post-thumbnail {
flex: 0 0 120px;
height: 120px;
overflow: hidden;
}
.post-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.post-thumbnail:hover img {
transform: scale(1.05);
}
.no-image {
width: 100%;
height: 100%;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 12px;
}
.post-content {
flex: 1;
padding: 15px;
display: flex;
flex-direction: column;
}
.post-title {
margin: 0 0 10px 0;
font-size: 16px;
line-height: 1.4;
}
.post-title a {
color: #333;
text-decoration: none;
}
.post-title a:hover {
color: #007cba;
}
.post-meta {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
font-size: 12px;
color: #666;
}
.post-categories a {
background: #007cba;
color: white;
margin-right: 5px;
padding: 2px 6px;
border-radius: 3px;
text-decoration: none;
font-size: 10px;
}
.post-categories a:hover {
background: #005a87;
}
.post-excerpt {
color: #666;
font-size: 14px;
line-height: 1.5;
margin-top: auto;
}
.pagination {
display: flex;
justify-content: center;
gap: 5px;
margin-top: 30px;
}
.page-number {
display: inline-block;
padding: 8px 12px;
border: 1px solid #ddd;
background: white;
color: #333;
text-decoration: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.page-number:hover {
background: #f5f5f5;
border-color: #999;
}
.page-number.active {
background: #007cba;
color: white;
border-color: #007cba;
cursor: default;
}
.loading, .error, .no-posts {
text-align: center;
padding: 40px 20px;
color: #666;
font-size: 16px;
}
.loading {
position: relative;
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
margin-left: 10px;
border: 2px solid #ddd;
border-top: 2px solid #007cba;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #dc3545;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
}
.no-posts {
background: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 4px;
color: #0c5460;
}
@media (max-width: 768px) {
.category-filter-controls {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.category-filter-select {
width: 100%;
}
.results-count {
margin-left: 0;
align-self: flex-start;
}
.category-filter-results {
grid-template-columns: 1fr;
}
.post-item {
flex-direction: column;
}
.post-thumbnail {
flex: none;
height: 200px;
}
.post-meta {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
}
</style>
<div class="category-filter-wrapper">
<!-- カテゴリー絞り込み検索ブロックを投稿ループの外に移動 -->
<h2>カテゴリー絞り込み検索</h2>
<p>以下のセレクトボックスからカテゴリーを選択すると、該当する記事を非同期で表示します。</p>
<?php
// カテゴリーフィルターを表示
$categories = get_categories( array(
'taxonomy' => 'category',
'hide_empty' => true,
'orderby' => 'name',
'order' => 'ASC',
) );
if ( ! empty( $categories ) ) :
?>
<div class="category-filter-container" data-posts-per-page="3">
<div class="category-filter-controls">
<label for="category-select">カテゴリーで絞り込み:</label>
<select id="category-select" class="category-filter-select">
<option value="0">すべてのカテゴリー</option>
<?php foreach ( $categories as $category ) : ?>
<option value="<?php echo esc_attr( $category->term_id ); ?>">
<?php echo esc_html( $category->name ); ?> (<?php echo esc_html( $category->count ); ?>)
</option>
<?php endforeach; ?>
</select>
<div class="results-count"></div>
</div>
<div class="category-filter-results-wrapper">
<?php
// 初期表示用の投稿を取得
$initial_query = new WP_Query( array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 3,
'paged' => 1,
) );
if ( $initial_query->have_posts() ) {
echo '<div class="category-filter-results">';
while ( $initial_query->have_posts() ) {
$initial_query->the_post();
?>
<article class="post-item">
<div class="post-thumbnail">
<?php if ( has_post_thumbnail() ) : ?>
<a href="<?php the_permalink(); ?>"><?php the_post_thumbnail( 'medium' ); ?></a>
<?php else : ?>
<a href="<?php the_permalink(); ?>">
<div class="no-image">画像なし</div>
</a>
<?php endif; ?>
</div>
<div class="post-content">
<h3 class="post-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="post-meta">
<time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>"><?php echo esc_html( get_the_date() ); ?></time>
<?php
$post_categories = get_the_category();
if ( $post_categories ) {
echo '<span class="post-categories">';
foreach ( $post_categories as $cat ) {
echo '<a href="' . esc_url( get_category_link( $cat->term_id ) ) . '">' . esc_html( $cat->name ) . '</a>';
}
echo '</span>';
}
?>
</div>
<div class="post-excerpt">
<?php echo wp_trim_words( get_the_excerpt(), 30 ); ?>
</div>
</div>
</article>
<?php
}
echo '</div>';
// ページネーション
if ( $initial_query->max_num_pages > 1 ) {
echo '<div class="pagination">';
for ( $i = 1; $i <= $initial_query->max_num_pages; $i++ ) {
$active_class = ( $i == 1 ) ? ' active' : '';
echo '<span class="page-number' . $active_class . '" data-page="' . $i . '">' . $i . '</span>';
}
echo '</div>';
}
} else {
echo '<div class="no-posts">投稿が見つかりませんでした。</div>';
}
wp_reset_postdata();
?>
</div>
</div>
<?php endif; ?>
<!-- 通常の投稿ループ(必要ならここに) -->
<?php /*
while ( have_posts() ) : the_post();
// 通常の投稿表示処理
endwhile;
*/ ?>
</div>
<script>
jQuery(document).ready(function($) {
'use strict';
// カテゴリーセレクトボックスの変更イベント
$(document).on('change', '.category-filter-select', function() {
var categoryId = $(this).val();
var container = $(this).closest('.category-filter-container');
loadPosts(categoryId, 1, container);
});
// ページネーションのクリックイベント
$(document).on('click', '.page-number', function(e) {
e.preventDefault();
if ($(this).hasClass('active')) {
return;
}
var page = $(this).data('page');
var container = $(this).closest('.category-filter-container');
var categoryId = container.find('.category-filter-select').val();
loadPosts(categoryId, page, container);
});
function loadPosts(categoryId, page, container) {
var resultsContainer = container.find('.category-filter-results-wrapper');
var postsPerPage = container.data('posts-per-page') || 10;
resultsContainer.html('<div class="loading">読み込み中...</div>');
$('html, body').animate({
scrollTop: container.offset().top - 100
}, 300);
$.ajax({
url: categoryFilterAjax.ajax_url,
type: 'POST',
data: {
action: 'category_filter',
category_id: categoryId,
posts_per_page: postsPerPage,
paged: page,
nonce: categoryFilterAjax.nonce
},
success: function(response) {
if (response.success) {
resultsContainer.html(response.data.content);
var countElement = container.find('.results-count');
if (countElement.length) {
var countText = response.data.found_posts + '件の記事が見つかりました';
countElement.text(countText);
}
resultsContainer.hide().fadeIn(300);
} else {
resultsContainer.html('<div class="error">エラーが発生しました。</div>');
}
},
error: function() {
resultsContainer.html('<div class="error">通信エラーが発生しました。</div>');
}
});
}
});
</script>
上記のコードは、WordPressで非同期(Ajax)によるカテゴリー絞り込み検索を実現するものです。まず、PHP側ではget_categories()関数を使ってカテゴリー一覧を取得し、セレクトボックスとして表示します。
投稿の初期表示にはWP_Queryを用いて3件の記事を取得し、一覧として出力します。また、最大ページ数に応じてページネーションの番号も出力されます。
また、JavaScriptでは、jQueryを用いてイベント処理を行っています。カテゴリーセレクトボックスが変更されると、選択されたカテゴリーIDとともにloadPostsという関数が呼び出され、Ajaxリクエストがadmin-ajax.phpへ送信されます。このとき、投稿数やページ番号、セキュリティ用のnonceも一緒に送られます。
リクエストに成功すると、返されたHTMLデータが結果表示エリアに動的に差し替えられ、フェードインで表示されます。
また、投稿件数の表示も更新されます。ページネーションをクリックしたときも同様の流れで再びloadPostsが呼ばれ、該当ページの投稿が非同期で読み込まれます。
コピー&ペーストができたら、次は以下のコードをfunctions.phpにコピー&ペーストしてください。
<?php
add_action('wp_enqueue_scripts', 'my_enqueue_category_filter_script');
function my_enqueue_category_filter_script() {
wp_enqueue_script('jquery');
// メインテーマのindex.phpで直接JSを書いている場合は、ダミーで空JSを登録
wp_register_script('category-filter-ajax', false);
wp_enqueue_script('category-filter-ajax');
wp_localize_script('category-filter-ajax', 'categoryFilterAjax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('category_filter_nonce')
));
}
// AJAXハンドラーの登録
add_action( 'wp_ajax_category_filter', 'category_filter_ajax_handler' );
add_action( 'wp_ajax_nopriv_category_filter', 'category_filter_ajax_handler' );
function category_filter_ajax_handler() {
// nonceチェック
if ( ! wp_verify_nonce( $_POST['nonce'], 'category_filter_nonce' ) ) {
wp_die( 'セキュリティエラー' );
}
$category_id = intval( $_POST['category_id'] );
$posts_per_page = intval( $_POST['posts_per_page'] ) ?: 10;
$paged = intval( $_POST['paged'] ) ?: 1;
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $posts_per_page,
'paged' => $paged,
);
// カテゴリーが指定されている場合
if ( $category_id > 0 ) {
$args['cat'] = $category_id;
}
$query = new WP_Query( $args );
ob_start();
if ( $query->have_posts() ) {
echo '<div class="category-filter-results">';
while ( $query->have_posts() ) {
$query->the_post();
?>
<article class="post-item">
<div class="post-thumbnail">
<?php if ( has_post_thumbnail() ) : ?>
<a href="<?php the_permalink(); ?>"><?php the_post_thumbnail( 'medium' ); ?></a>
<?php else : ?>
<a href="<?php the_permalink(); ?>">
<div class="no-image">画像なし</div>
</a>
<?php endif; ?>
</div>
<div class="post-content">
<h3 class="post-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="post-meta">
<time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>"><?php echo esc_html( get_the_date() ); ?></time>
<?php
$categories = get_the_category();
if ( $categories ) {
echo '<span class="post-categories">';
foreach ( $categories as $category ) {
echo '<a href="' . esc_url( get_category_link( $category->term_id ) ) . '">' . esc_html( $category->name ) . '</a>';
}
echo '</span>';
}
?>
</div>
<div class="post-excerpt">
<?php echo wp_trim_words( get_the_excerpt(), 30 ); ?>
</div>
</div>
</article>
<?php
}
echo '</div>';
// ページネーション
if ( $query->max_num_pages > 1 ) {
echo '<div class="pagination">';
for ( $i = 1; $i <= $query->max_num_pages; $i++ ) {
$active_class = ( $i == $paged ) ? ' active' : '';
echo '<span class="page-number' . $active_class . '" data-page="' . $i . '">' . $i . '</span>';
}
echo '</div>';
}
} else {
echo '<div class="no-posts">該当する記事が見つかりませんでした。</div>';
}
wp_reset_postdata();
$content = ob_get_clean();
wp_send_json_success( array(
'content' => $content,
'found_posts' => $query->found_posts,
'max_num_pages' => $query->max_num_pages,
) );
}
上記のコードは、WordPressのAjax機能を使って、カテゴリーによる投稿の絞り込みを非同期で行う処理を行なっています。主にJavaScriptへのデータ受け渡しの設定と、サーバー側でのAjaxリクエストの処理を行います。
まず、独自に定義したmy_enqueue_category_filter_script()関数では、jQueryの読み込みとともに、空のJavaScriptファイル(category-filter-ajax)を登録・読み込みしています。
これは、index.phpなどに直接JavaScriptを記述していてもwp_localize_script()関数が動作するようにするための工夫になります。wp_localize_script()関数によって、Ajax通信に必要なadmin-ajax.phpのURLや、セキュリティ用のnonce(使い捨てトークン)をJavaScript側へ渡しています。
次に、独自に定義したcategory_filter_ajax_handler()関数は、Ajaxリクエストに応じて動作する処理で、WordPressの「wp_ajax_〜」と「wp_ajax_nopriv_〜」の2つのアクションフックに登録されています。前者はログイン中のユーザー用、後者は非ログインユーザー用です。
関数内ではまず、受け取ったnonce(使い捨てトークン)の整合性をチェックして不正アクセスを防ぎます。その後、POSTで送られてきたカテゴリーID、表示件数、ページ番号を取得し、それに基づいて投稿を検索するクエリ(WP_Query)を実行します。
該当する投稿がある場合は、それぞれの記事をHTMLとして出力し、必要であればページネーションも追加します。出力内容はob_start()関数とob_get_clean()関数でバッファリングされ、最後にwp_send_json_success()関数でJSON形式のレスポンスとして返されます。このレスポンスはJavaScript側で受け取られ、画面に表示される仕組みです。
ここまでの処理を実装することにより、ユーザーがセレクトボックスでカテゴリーを選ぶと、選ばれたカテゴリーに応じた記事一覧が、ページ遷移なしに非同期で表示されるようになります。
ちなみにフックについてよくわからない、また詳しく知りたい方は、よろしければ以下の記事をご確認ください。
ここまで完了したら、ご自身で実装したページをご確認ください。
すると下図のように「カテゴリー絞り込み検索」の記事一覧のUIが表示されていることと思います。

動きも以下の動画のようにちゃんと動いているでしょうか?
はい、これでAjaxで非同期通信のページネーションをWordPressループ(記事一覧)のカテゴリー絞り込みで実装する方法の解説は以上になります。お疲れ様でした!
今回の内容によって、Ajaxによる非同期のページネーションの実装方法と、またそれに合わせてWordPressループをカテゴリーで絞り込みさせる実装方法が理解できたかと思います。
もし処理の内容や見た目のデザインなどをもう少し変えたいな〜と言う方は、ご自身でご自由にカスタマイズしていただいて問題ありませんので、ぜひ挑戦してみてください。
まとめ
今回はWordPressループにおけるカテゴリー絞り込み検索を、Ajaxを用いて非同期通信で実装し、さらにページネーションも非同期で連動させる方法について解説しました。
個人的にもこの実装には長らく課題を感じており、これまで絞り込み検索とAjaxによるページネーションの両方を組み合わせた具体的な情報が少なかったことから、今回の公開に大きな意義を感じています。
特にAjaxによるページネーションは実装難易度が高く、以前はChatGPTを使ってもなかなか満足のいく実装に至らなかったため、この記事が同じような課題を持つエンジニアの方にとって、実用的なヒントや参考になることを願っています。
実務や個人プロジェクトでAjaxを使った高度な絞り込み検索機能を導入したい方は、ぜひ今回の内容を活用してみてください。
お気軽に皆さんのご要望をお聞かせください!
どんなに些細なことでも構いません!よろしければ記事や当サイトへの「こんな記事があったら仕事とかで役に立つな〜」や「こうだったらもっと役に立つのに!」といったようなご要望等をお気軽にお聞かせください!今後のサービス改善にお役立てさせていただきます!
例1)Reactの技術記事を書いてほしい!
例2)WordPressの使い方とかを初心者向けに解説してほしい!...など