One of the most powerful features of WordPress is the WordPress Query. It is what determines what content is displayed on what page. And often you’ll want to modify this query to your specific needs.
For example, you might want to:
- Exclude posts from a certain category on the homepage
- Increase or decrease the number of posts displayed per page for a specific post type
- Sort posts in your category archive by comment count rather than post date
- Exclude posts marked “no-index” in Yoast SEO from your on-site search results page
- List related articles at the end of a post.
If you’re interested in looking “under the hood” at how queries in WordPress work, here’s the slides from a presentation by WordPress Lead Developer Andrew Nacin. I’m going to focus this tutorial on common uses.
First, what not to do.
Don’t use query_posts()
The query_posts() function completely overrides the main query on the page and can cause lots of issues. There is no instance where query_posts() is recommended.
Depending upon your specific requirements, you should create a new WP_Query or customize the main query.
If you need a simple way to list dynamic content on your site, try my Display Posts Shortcode plugin. It works just like the custom query described below but is built using a shortcode so you don’t have to write any code or edit your theme files.
Customize the Main Query
The “main query” is whatever WordPress uses to build the content on the current page. For instance, on my Genesis category archive it’s the 10 most recent posts in that category. The first four examples above all require altering the main query.
We’ll use the WordPress hook pre_get_posts to modify the query settings before the main query runs. You must your function in your theme’s functions.php or a core functionality plugin. WordPress needs to build the query to figure out what template to load, so if you put this in a template file like archive.php it will be too late.
All our functions are going to have a similar structure. First we’re going to make sure we’re accessing the main query. If we don’t check this first our code will affect every query from nav menus to recent comments widgets. We’ll do this by checking $query->is_main_query().
We’ll also this code isn’t running on admin queries. You might want to exclude a category from the blog for your visitors, but you still want to access those posts in the Posts section of the backend. To do this, we’ll add ! is_admin().
Then we’ll check to make sure the conditions are right for our modification. If you only want it on your blog’s homepage, we’ll make sure the query is for home ( $query->is_home() ). Here’s a list of available conditional tags.
Finally, we’ll make our modification by using the $query->set( 'key', 'value' ) method. To see all possible modifications you can make to the query, review the my WP_Query arguments guide or the WP_Query Codex page.
Exclude Category from Blog
/**
* Exclude Category from Blog
*
* @author Bill Erickson
* @link https://www.billerickson.net/customize-the-wordpress-query/
* @param object $query data
*
*/
function be_exclude_category_from_blog( $query ) {
if( $query->is_main_query() && ! is_admin() && $query->is_home() ) {
$query->set( 'cat', '-4' );
}
}
add_action( 'pre_get_posts', 'be_exclude_category_from_blog' );
We’re checking to make sure the $query is the main query, and we’re making sure we’re on the blog homepage using is_home(). When those are true, we set ‘cat’ equal to ‘-4’, which tells WordPress to exclude the category with an ID of 4.
Change Posts Per Page
Let’s say you have a custom post type called Event. You’re displaying events in three columns, so instead of the default 10 posts per page you want 18. If you go to Settings > Reading and change the posts per page, it will affect your blog posts as well as your events.
We’ll use pre_get_posts to modify the posts_per_page only when the following conditions are met:
- On the main query
- Not in the admin area (we only want this affecting the frontend display)
- On the events post type archive page
/**
* Change Posts Per Page for Event Archive
*
* @author Bill Erickson
* @link https://www.billerickson.net/customize-the-wordpress-query/
* @param object $query data
*
*/
function be_change_event_posts_per_page( $query ) {
if( $query->is_main_query() && !is_admin() && is_post_type_archive( 'event' ) ) {
$query->set( 'posts_per_page', '18' );
}
}
add_action( 'pre_get_posts', 'be_change_event_posts_per_page' );
Modify Query based on Post Meta
This example is a little more complex. We want to make some more changes to our Event post type. In addition to changing the posts_per_page, we want to only show upcoming or active events, and sort them by start date with the soonest first.
I’m storing Start Date and End Date in postmeta as UNIX timestamps. With UNIX timestamps, tomorrow will always be a larger number than today, so in our query we can simply make sure the end date is greater than right now.
Here’s more information on building Custom Metaboxes. My BE Events Calendar plugin is a good example of this query in practice.
If all the conditions are met, here’s the modifications we’ll do to the query:
- Do a meta query to ensure the end date is greater than today
- Order by meta_value_num (the value of a meta field)
- Set the ‘meta_key’ to the start date, so that’s the meta field that posts are sorted by
- Put it in ascending order, so events starting sooner are before the later ones
/**
* Customize Event Query using Post Meta
*
* @author Bill Erickson
* @link http://www.billerickson.net/customize-the-wordpress-query/
* @param object $query data
*
*/
function be_event_query( $query ) {
if( $query->is_main_query() && !$query->is_feed() && !is_admin() && $query->is_post_type_archive( 'event' ) ) {
$meta_query = array(
array(
'key' => 'be_events_manager_end_date',
'value' => time(),
'compare' => '>'
)
);
$query->set( 'meta_query', $meta_query );
$query->set( 'orderby', 'meta_value_num' );
$query->set( 'meta_key', 'be_events_manager_start_date' );
$query->set( 'order', 'ASC' );
$query->set( 'posts_per_page', '4' );
}
}
add_action( 'pre_get_posts', 'be_event_query' );
Create a new query to run inside your page or template.
All of the above examples showed you how to modify the main query. In many instances, you’ll want to run a separate query to load different information, and leave the main query unchanged. This is where Custom WordPress Queries are useful.
This is best when the content you’re displaying is being loaded in addition to your current page’s content. For instance, if you had a page about Spain and wanted to show your 5 most recent blog posts about Spain at the bottom, you could do something similar to this:
/**
* Display posts about spain
*
*/
function be_display_spain_posts() {
$loop = new WP_Query( array(
'posts_per_page' => 5,
'category_name' => 'spain',
) );
if( $loop->have_posts() ):
echo '<h3>Recent posts about Spain</h3>';
echo '<ul>';
while( $loop->have_posts() ): $loop->the_post();
echo '<li><a href="' . get_permalink() . '">' . get_the_title() . '</a></li>';
endwhile;
echo '</ul>';
endif;
wp_reset_postdata();
}
add_action( 'genesis_after_entry', 'be_display_spain_posts' );
For this you’ll use the WP_Query class. For more information, see my post on Custom WordPress Queries. I also have a WP_Query Arguments reference guide.
Gary Darling says
Great tutorial Bill. I’m trying to implement this on a site that uses `query_posts` in several template files, but I’m struggling with a custom query. The user wants his posts sorted by first by the meta_field ‘builder’, then by the meta_field ‘squareft’. So the result would be all builders named ‘A’ would be first, and within that group all his homes ordered by ‘2000’, ‘2100’, etc.
I’ve tried variations of Orderby with two meta_values, with ‘title’ and ‘meta_value’, I tried two meta_keys, no luck. Any ideas? Here is the code I have now:
http://gist.github.com/3678753
Bill Erickson says
I don’t believe you can have WordPress sort the results by two keys like that. You should build your query, then run through it once or twice to manually sort it the way you want, then output it.
Devplus says
Thanks so much Bill for the tutorial.
I looked at your example on excluding a category from the loop above, great! How do I exclude an array of post format from the loop? Let say I want to exclude aside and quote post format from the loop. Hope I didn’t ask you too much.
Bill Erickson says
Post formats is actually a taxonomy called ‘post_format’. So you would use a tax_query to interact with it.
Here’s how you’d exclude aside and quote from the homepage: https://gist.github.com/3698518
Devplus says
Thanks so much for the code.
However, I’ve problem with the code. My blog simply doesn’t show any blogpost with the message:
“Sorry, no posts matched your criteria.”
Here’s what I’ve in my functions.php https://gist.github.com/3712088 . Nothing much there, a fresh functions.php file from Sample Child Theme. I add support for post format and just paste your code above at the very bottom of the file.
I’ve tried two ways:
1. Since I’ve no home.php file, so I just leave it as it is. By default, genesis will show the loop of latest blogspot. No luck.
2. Secondly, I tried creating a “blog” page, using the blog template. Then, from the reading settings page, I tick on the static page, and from the dropdown of “post page”, I chose the “blog” page I’ve created previously. No luck.
Did I miss something here?
Bill Erickson says
Sorry, I forgot to wrap the tax_query in an extra array(). I’ve updated the code ( https://gist.github.com/3714550 ), and just tested it to confirm it works.
Nicholas says
Thank you for such great tutorial, very detailed explanation on how to use pre_get_posts.
However, can pre_get_posts be used to be applied only to one custom query?
Because, i want to list all posts with closed comments, only in one custom query that runs on one of my custom page templates.
On another page i want to list all posts with opened comments, and on a third page i want to list all posts.
Therefore, i’m using three different custom queries, the last one is simple to make, but i’m having problem with the first two – can i apply pre_get_posts on two different custom queries separately?
Bill Erickson says
pre_get_posts should only be used for the main page’s query (so on a custom page template, rendering the content entered into the Edit Page screen). For what you’re looking to do, you’ll want to make a custom query using WP_Query().
Nicholas says
Thanks for the reply,
but how can i include the comment status into a custom query?
Under $args for WP_Query, there is nothing related to comment status?
I have this bit of code https://gist.github.com/3709991
Here, i display 10 posts per page, and for each post check if the comments are open – but that breaks my pagination – if among 10 posts, there are no posts with open comments, it will display a blank page, and i don’t want that.
Or, if there are 2 posts with open comments among 10 posts, it will display 2 posts on a page.
I want to list all posts with open comments, 10 posts per page.
Bill Erickson says
It doesn’t look like comment status is an available query arg. You’ll need to craft a custom SQL query that looks for posts with ‘open’ in the ‘comment_status’ column of the posts table.
Nicholas says
Hm, but here http://codex.wordpress.org/Plugin_API/Action_Reference/pre_get_posts it says comment_status can be used in pre_get_posts.
So, if i would to aim at all queries, when i put $query->set( ‘comment_status’, ‘open’ ); it should display posts with open comments on every loop in the blog? But, it doesn’t work? Can you clarify?
Bill Erickson says
I don’t see anywhere on that page where it says comment_status can be used with pre_get_posts. The only place comment_status is located on that page is in showing what is returned in the $post.
Brian McFarlin says
Bill,
I used your “Better, and easier grid loop” tutorial to display my CPT in a grid format (worked great, by the way). Now I’m simply trying to add those CPT posts to the main query.
After reading your tutorial on customizing the query, I’m still not clear on how to simply include a category. If cat,-4 excludes a given category, does cat,+4 include one?
Bill Erickson says
Are you trying to mix in your custom post type posts (ex: ‘products’ post type) into your blog ( the ‘post’ post type)? If so, use $query->set( ‘post_type’, array( ‘post’, ‘products’ ) )
Brian McFarlin says
Yes…that is what I’m trying to do. I’ll try the code you provided.
One more question:
On the corresponding add_action, could I use pre-get_posts?
Bill Erickson says
Yes, here’s the completed code: https://gist.github.com/4583089b8fdb8694fe30
Lucas says
Hi Bill,
I’m looking to add a CPT to my main blog page in genesis (which is not the home page). When I try to add in is_page(‘archives’), the main blog archives page turns to to a 404 page.
Here’s the code. Do you have any suggestions?
function namespace_add_custom_types( $query ) {
if( $query->is_main_query() || is_category() || is_tag() && empty( $query->query_vars['suppress_filters'] ) ) {
$query->set( 'post_type', array(
'post', 'tips', 'nav_menu_item'
));
return $query;
}
}
add_filter( ‘pre_get_posts’, ‘namespace_add_custom_types’ );
Bill Erickson says
A few issues:
1. You need to make sure you’re always affecting the main query. You used an OR in there, not an AND.
2. You never targeted the main blog page ( is_home() )
3. I don’t think you want ‘nav_menu_item’ showing up in your blog archives.
4. You don’t need to return the query. This is an action, not a filter.
Here’s updated code: https://gist.github.com/billerickson/8919367
Lucas Hall says
Hi Bill,
That code snippet is so much cleaner! Thank you.
However, it still didn’t add a CPT to the main blog page:
http://www.landlordlogy.com/articles.
I’m using genesis, and the Article page is using the page_blog.php page template. Too bad there isn’t a condition called “$query->is_blog()”.
I’ve tried adding $query->is_page_tempate( ‘page_blog.php’), and $query->is_page( 1207 ), to your code, but when I do, the Articles page turns into a 404.
Do you have any other advice?
Thanks in advance. I really appreciate it!
Bill Erickson says
Never use the blog page template in Genesis. Just don’t do it.
Go to Settings > Reading and set your blog page as the posts page. If your theme is written incorrectly (as many StudioPress themes are) and uses home.php as a widgetized front page, go into your theme files and rename that to front-page.php.
I’ll be writing a blog post about things not to use in Genesis, and the Blog page template is #1.
Lucas Hall says
Hi Bill,
Thanks for the clarification! I got it working now. Why don’t the folks at studiopress follow this practice instead of making their themes with a blog template. Maybe you’ll answer that in your upcoming post.
Thanks again!
Lucas
Jason Judge says
Don’t forget, $query->set( ‘meta_query’, …) will obliterate any meta query conditions that have already been set up by other plugins and filters. You need to get the current meta_query array (which may not be set at all, so needs initialising to an array if not) using $query->get( ‘meta_query’) then merge your conditions into that.
— Jason
Lee says
Our home.php doesn’t require any default posts query. I’m a little lost on how to skip the main query entirely.
So far the best I’ve been able to come up with is set
posts_per_pageto 1 (because if it is 0 it gets set back to the default). Any thoughts on how one might cancel the query frompre_get_posts?Bill Erickson says
home.php is used for displaying your latest posts. If you don’t want to display posts, you shouldn’t be using home.php. If this is for the front page of your site, go to Settings > Reading and select a page to be on the homepage.
If for some reason you must use home.php but you don’t want posts listed, find where the loop is being generated in that template file and remove it. In genesis, you’d accomplish this by adding the following to home.php:
remove_action( 'genesis_loop', 'genesis_do_loop' );Lee says
I hear you about the static homepage option. The problem for us is that we still want the query to run for paged requests (e.g. /page/2).
Here is what I have in a
pre_get_postsfilter. It more or less works:if ($query->is_main_query() && $query->is_home && !$query->is_paged) {
$query->is_home = false;
$query->is_page = true;
$query->is_posts_page = false;
$query->is_singular = true;
$query->set(“page_id”, HOME_PAGE_ID); // constant set in wp-config.php
$query->set(“post_type”, “page”);
}
Thanks for the genesis loop tip, I’ll give that shot.
Bill Erickson says
What are you using the paged requests for if you’re not displaying posts? In any case, those paged requests work on static pages as well.
Simon says
Hi Bill, when using the genesis loop, when a post is set as sticky, (for display on the homepage), the post also appears on page 2 (i.e. the posts overflowing from the front page onto subsequent pages).
I’m thinking one way to remove the sticky from the overflow (page 2) listing, would be to assign it to a category–& then exclude that category from the standard genesis loop display.
I writing to ask how I would do that–&/or if there’s a better way to achieve the same.
Thanks,
Simon
Bill Erickson says
You can use the ‘ignore_sticky_posts’ parameter to exclude them: http://codex.wordpress.org/Class_Reference/WP_Query#Sticky_Post_Parameters
Simon says
Thanks Bill! Works perfect!
Simon
Simon says
Actually Bill, there is one other issue that’s arises from the use of:
‘ignore_sticky_posts’ => 1
Because there’s a single sticky post at the top of the home page, (which the template layout being utilised) it means one extra post is present on the home page compared to the overflow pages–which means the layout is affected on page 2, 3, 4, etc.
Bill Erickson says
I don’t think there’s anything you can do about this. Why not just drop the sticky posts altogether? Or, add ignore_sticky_posts for all pages (I assume you’re only doing it for inner pages), then add a new custom query to the top that only runs on the first page and only displays the sticky post.
Simon says
The homepage sticky’s such an integral part of the design–aesthetics wise. I need the single sticky to count for two posts. Is there a counter I can use to achieve this.
What you’ve written (below) sounds like a possibility, (if I can get the sticky using it’s unique display formatting). How would I create/incorporate this query:
===
add a new custom query to the top that only runs on the first page and only displays the sticky post
===
Mary says
Hi Bill! First, thank you so much for your tutorials on here, they have helped so much! I am learning php on the fly, and am trying to do something specific on my archive page using the grid loop. I believe I have to use a custom query and I’m clueless, so I’m here to beg for help lol!
Basically, I would like an archive page that shows the title and featured image for each post, but I would like all posts displayed by category. I don’t know if I am just being a glutton for punishment, but I’m really intrigued to see if this is possible, and how.
My archive page template is set up as follows:
https://gist.github.com/4562711
I have it set up so I choose the categories displayed on the page via the: “genesis_get_custom_field(‘query_args’); / custom field setting on the page.
I have tried using the wp codex, and adding
echo ” . get_cat_name( $cat_id ) . ”;
to the query section, but it doesn’t work… maybe I am putting it in the wrong place?
Just to reiterate, I am quite clueless (sorry!) but I’d like the archive page to be set up so that each category is displayed on the page, ALONG WITH every post (title & image) in that category. In short, multiple categories displayed, showing all posts in those categories, all on 1 handy-dandy page. I hope I explained that properly? Any advice would be much appreciated (or feel free to let me know if I really am being a glutton for punishment trying to do this!)
Thanks so much.
Bill Erickson says
Your description sounds like you want to loop through all categories and display all of their posts, but the code you provided looks like you want to query a specific category.
1. If you’re trying to display only the posts from a single category, and specifying that category in the backend using a custom field, your template would look like this: https://gist.github.com/4572940
2. If you want a template file that loops through all categories, and for each category shows all their posts, your template file would look like this: https://gist.github.com/4572998
Patrick says
Hi BIll,
I used the pre_get_posts method to change the post_type used on my home page to a custom post type I have. This works great and avoids having to perform more queries on this page, however on other pages I use template files and would love to be able to target these pages in a similar way via functions.php and alter the main query as I have done with the home page.
However, I tried simply changing if ( $query->is_main_query() && $query->is_home() ) { to if ( $query->is_main_query() && $query->is_page(ID) ) { but this doesn’t work. I’m sure there must be a way of using pre_get_posts on template pages, by altering the main query but I can’t work out how to do it.
Any help would be appreciated as I really notice the difference in speed, thanks.
Bill Erickson says
I’ve never used it on template pages because in my experience, template pages are typically secondary queries, so I use WP_Query().
In your template, put this:
That will tell you everything that’s currently in the query. You can then use the pre_get_posts method to remove everything you don’t want in there.
Robert says
I’m trying to remove certain categories from my RSS feed, can you tell me if I can use $query->set(‘cat’, ‘-99’); multiple times, or should I use an array?
https://gist.github.com/4692710
Thanks!
Bill Erickson says
I think you need to use an array. Doing that multiple times just overwrites it each time.
Robert says
Thanks! I think I have it working now with $query->set(‘cat’, ‘-111,-135,-138,-143’); – updated the gist to reflect.