Custom queries: WP_Query
The main Loop prints what WordPress selected from the page address. But the front page also needs the three latest books, and an article needs related posts below it. The WP_Query class serves such selections.
A second loop
<?php
// front-page.php
$books = new WP_Query([
'post_type' => 'book',
'posts_per_page' => 3,
]);
if ($books->have_posts()) :
while ($books->have_posts()) :
$books->the_post();
?>
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<?php
endwhile;
wp_reset_postdata();
endif;The have_posts() and the_post() methods are called on the query object. After the loop wp_reset_postdata() restores the post that was current before it.
Common parameters
| Parameter | What it sets |
|---|---|
post_type | The post type: post, page, a custom type |
posts_per_page | How many posts; -1 means all |
orderby, order | Ordering: date, title, rand, meta_value_num; ASC or DESC |
category_name | A category slug |
s | A search phrase |
post__in, post__not_in | Only these identifiers, or all except them |
paged | The page number |
author | The author's identifier |
Selecting by taxonomy
$query = new WP_Query([
'post_type' => 'book',
'tax_query' => [
[
'taxonomy' => 'genre',
'field' => 'slug',
'terms' => ['fantasy', 'detective'],
],
],
]);Selecting by a field
$query = new WP_Query([
'post_type' => 'book',
'meta_key' => 'book_price',
'orderby' => 'meta_value_num',
'order' => 'ASC',
'meta_query' => [
[
'key' => 'book_price',
'value' => 20,
'compare' => '<=',
'type' => 'NUMERIC',
],
],
]);The query returns books costing 20 or less, ordered by price.
Related posts
<?php
// single.php
$related = new WP_Query([
'category__in' => wp_get_post_categories(get_the_ID()),
'post__not_in' => [get_the_ID()],
'posts_per_page' => 3,
]);Just an array of posts
When no loop is needed, get_posts() is handier — it returns an array of objects:
$latest = get_posts(['post_type' => 'book', 'numberposts' => 5]);
foreach ($latest as $book) {
echo esc_html($book->post_title);
}Changing the main query
To change what the main Loop prints, you do not create a new query but adjust the existing one — with the pre_get_posts hook:
// functions.php
add_action('pre_get_posts', function (WP_Query $query) {
if (is_admin() || !$query->is_main_query()) {
return;
}
if ($query->is_post_type_archive('book')) {
$query->set('posts_per_page', 12);
$query->set('orderby', 'title');
$query->set('order', 'ASC');
}
});Pagination keeps working correctly.
Common mistake
Without wp_reset_postdata() after your own loop, the_title() and get_the_ID() further down the template refer to the last post of your query: someone else's comments and title appear under the article. And do not use the old query_posts() function: it breaks pagination — pre_get_posts exists for the main query.