2026年9月25日
2026年9月25日
WordPressのWP_Queryで記事を取得する方法:基本のループから絞り込み・ページ送り・高速化まで
WP_Query は、条件を指定して投稿を取り出し、テンプレートの中で一覧表示するための WordPress コアのクラスです。この記事では、新着記事やカテゴリー別一覧をつくる基本のループから、タクソノミー・カスタムフィールドでの絞り込み、ページ送り、重くしないための引数までを順に説明します。読み終えるころには「どこに書くか」「メインクエリを変えるべきか新しく作るべきか」を自分で判断できるようになります。
WP_Queryを使う前に決めておくこと
最初に考えたいのは、「ページ本来の一覧を変えたいのか」「ページの一部に別の一覧を足したいのか」という点です。
- アーカイブやトップの一覧そのもの(表示件数、並び順、除外したい投稿)を変えたい場合は、新しい WP_Query を作らず、
pre_get_postsフックでメインクエリを調整します。ここで WP_Query を別に作ると、ページ送りが404になる、同じ問い合わせが二重に走るといった問題が起きがちです。 - サイドバーの新着5件、記事下の関連記事、固定ページ内のお知らせ一覧のように「追加の一覧」を出したい場合が、WP_Query の出番です。
query_posts() はメインクエリを上書きしてしまうため、公式ドキュメントでも使わないよう案内されています。古い解説記事で見かけても置き換えてください。
書く場所は、テーマのテンプレートなら子テーマのテンプレートファイル、テーマを替えても残したい機能ならプラグイン(または mu-plugin)です。親テーマを直接編集すると更新で消えます。作業前にはバックアップを取り、ステージング環境で表示を確かめてから本番に反映してください。
基本のループ:カテゴリー「news」の新着5件
<?php
$news_query = new WP_Query(
array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 5,
'category_name' => 'news', // カテゴリーのスラッグ
'ignore_sticky_posts' => true,
'no_found_rows' => true, // ページ送りしないので総件数は数えない
)
);
if ( $news_query->have_posts() ) : ?>
<ul class="news-list">
<?php while ( $news_query->have_posts() ) : $news_query->the_post(); ?>
<li>
<time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>"><?php echo esc_html( get_the_date() ); ?></time>
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</li>
<?php endwhile; ?>
</ul>
<?php else : ?>
<p>お知らせはまだありません。</p>
<?php endif;
wp_reset_postdata();
押さえておきたい点は3つです。
- **
$news_query->the_post()を呼ぶ**。これでグローバルな$postが切り替わり、the_title()やget_the_date()がループ中の投稿を指すようになります。have_posts()だけで回すと無限ループになります。 - **ループの後に
wp_reset_postdata()**。忘れると、その後ろにある本文やコメント欄が最後に取り出した投稿の情報で表示される、という分かりにくい不具合になります。 - 出力はエスケープする。属性値には
esc_attr()、テキストにはesc_html()、URL にはesc_url()を使います。the_permalink()は内部で URL をエスケープして出力します。
category_name にはカテゴリー名ではなくスラッグを渡します。ID で指定したい場合は cat を使います。
よく使う引数の整理
| 目的 | 引数の例 |
|---|---|
| 投稿タイプ | 'post_type' => 'event'(配列で複数可) |
| 件数 | 'posts_per_page' => 10 |
| 並び順 | 'orderby' => 'date'、'order' => 'DESC' |
| 特定の投稿を除外 | 'post__not_in' => array( get_the_ID() ) |
| タグ | 'tag' => 'wordpress'(スラッグ) |
| 投稿者 | 'author' => 3 |
posts_per_page に -1 を渡すと全件取得になります。記事が数千件ある環境ではメモリと時間を大きく使うので、上限を決めて取り出すほうが安全です。
タクソノミーとカスタムフィールドで絞り込む
カスタム投稿タイプ「event」のうち、地域タクソノミーが東京・大阪で、開催日が今日以降のものを開催日の近い順に並べる例です。開催日は 20261028 のような Ymd 形式でカスタムフィールド event_date に入っている前提です。
<?php
$events = new WP_Query(
array(
'post_type' => 'event',
'posts_per_page' => 10,
'tax_query' => array(
array(
'taxonomy' => 'event_area',
'field' => 'slug',
'terms' => array( 'tokyo', 'osaka' ),
),
),
'meta_query' => array(
array(
'key' => 'event_date',
'value' => current_time( 'Ymd' ),
'compare' => '>=',
'type' => 'NUMERIC',
),
),
'meta_key' => 'event_date',
'orderby' => 'meta_value_num',
'order' => 'ASC',
)
);
つまずきやすいのは型です。type を指定しないとカスタムフィールドは文字列として比較されるため、数値や日付の大小比較が意図どおりになりません。並べ替えも、数値として並べたいときは meta_value_num を使います。
tax_query と meta_query は配列の中に配列を入れる二重構造です。条件を複数並べるときは、同じ階層に 'relation' => 'AND'(または 'OR')を置きます。
検索フォームなど利用者の入力を条件に使う場合は、受け取った値をそのまま渡さないでください。ID は absint()、スラッグは sanitize_key() や sanitize_title() で整え、orderby のように選択肢が決まっている値は許可リストと照合してから使います。
ページ送りをつける
固定ページのテンプレート内で独自の一覧にページ送りをつける例です。
<?php
$paged = max( 1, absint( get_query_var( 'paged' ) ), absint( get_query_var( 'page' ) ) );
$list = new WP_Query(
array(
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => $paged,
)
);
while ( $list->have_posts() ) {
$list->the_post();
the_title( '<h3><a href="' . esc_url( get_permalink() ) . '">', '</a></h3>' );
}
$pagination = paginate_links(
array(
'total' => $list->max_num_pages,
'current' => $paged,
)
);
if ( $pagination ) { // 1ページしかないときは何も返らない
echo wp_kses_post( $pagination );
}
wp_reset_postdata();
固定ページを「ホームページの表示」に指定している場合、ページ番号は paged ではなく page に入ることがあるため、両方を見ています。ページ送りをする一覧では総件数が必要なので、no_found_rows は付けません。
カテゴリーアーカイブなどで「2ページ目以降が404になる」場合は、WP_Query を別に作っているのが原因であることが多いです。WordPress はメインクエリの件数でページの有無を判定するため、pre_get_posts で件数を合わせます。
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( $query->is_post_type_archive( 'event' ) ) {
$query->set( 'posts_per_page', 12 );
}
} );
is_admin() と is_main_query() の判定を省くと、管理画面の投稿一覧やウィジェットの問い合わせまで書き換えてしまうので注意してください。
重くしないための引数
WP_Query は便利な反面、条件しだいで重い SQL を発行します。表示に必要なものだけを取り出すのが基本です。
- **
'no_found_rows' => true**:ページ送りが不要なら、総件数を数える処理を省けます。 - **
'fields' => 'ids'**:ID だけ欲しい場合(件数確認、別処理への受け渡し)に使います。 - **
'update_post_meta_cache' => false/'update_post_term_cache' => false**:ループ内でカスタムフィールドやタームを使わないなら、まとめ読み込みを省けます。 - **
'orderby' => 'rand'は避ける**:記事数が多いと全件を並べ替えるため遅くなります。 - **大きなサイトでの
meta_query**:wp_postmetaは件数が膨らみやすく、値での絞り込みは遅くなりがちです。頻繁に絞り込む条件はタクソノミーで持つ設計も検討してください。
実際にどんな SQL が走っているかは、$list->request を開発環境で出力するか、Query Monitor プラグインで確認できます。遅い問い合わせが見つかったら、引数の見直しを先に行い、インデックスの追加などデータベース側の対処はその後に検討するのが安全な順序です。
動かないときの確認ポイント
- 何も表示されない:
post_typeやpost_statusが合っているか、スラッグ指定の値が正しいかを確認します。$query->found_posts(no_found_rowsを外した状態)で件数を見ると切り分けが早くなります。 - ループ後の表示がおかしい:
wp_reset_postdata()の呼び忘れを疑います。 - 固定表示の投稿が先頭に混ざる:
ignore_sticky_postsをtrueにします。 - 画面が真っ白になった:PHP の構文エラーが多いので、
WP_DEBUG_LOGを有効にしてwp-content/debug.logを確認し、FTP などで追記箇所を戻します。
WP_Query の引数はバージョンアップで追加されることがあるため、細かい挙動は WordPress 開発者リソースの WP_Query の項(2026年10月現在)で確認してから使ってください。
まとめ
- ページ本来の一覧を変えるなら
pre_get_posts、追加の一覧を出すなら WP_Query。query_posts()は使わない。 - ループでは
the_post()を呼び、終わったらwp_reset_postdata()。出力はesc_html()などでエスケープする。 - カスタムフィールドの比較や並べ替えは
typeとmeta_value_numで型を合わせる。利用者の入力はサニタイズと許可リストで受ける。 - ページ送りが不要なら
no_found_rows、ID だけならfields => 'ids'で負荷を下げる。 - 編集は子テーマかプラグインで行い、バックアップとステージングでの確認を済ませてから本番に反映する。