One of the most powerful features of WordPress is the WP 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. Some examples:
- Don’t display posts from Category X on the homepage
- Increase or decrease the number of posts displayed per page for a specific post type
- Determine which posts are shown and their order based on postmeta
- Inside your page, do a separate query for different content
Andrew Nacin recently gave a great talk on using the query, which you can look at for some more technical information on what’s happening behind the scenes. I’m going to focus this tutorial on common uses.
First, what not to do. Don’t use query_posts(). As you can see from the Codex page, there’s a lot of caveats to it. I really can’t think of an instance where this function is advisable.
There’s two approaches you should take depending on your needs.
Create a new query to run inside your page or template.
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.
For this you’ll use the WP_Query class. For more information, see my post on Custom WordPress Queries. Also take a look at my Display Posts Shortcode plugin, which might save you from having to write any code.
Customize the Main Query
If you want to alter which content is returned on a page, you most likely will be modifying the main query. The first three examples above all require altering the main query.
WordPress has a very handy hook called pre_get_posts. It fires once all the query settings are ready but right before the actual query takes place. This is where we’ll jump in and modify the query settings if needed.
You must place your function in functions.php (or a 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 everything from nav menus to recent comments widgets. We’ll do this by checking $query->is_main_query(). This was added in WordPress 3.3. For earlier versions, compare $query to the global $wp_the_query variable, as shown below in the 3.2 code below.
Then we’ll check to make sure the conditions are right for our modification. If you only want it on the homepage, we’ll make sure the query is for home ( $query->is_home() ).
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 WP_Query Codex page.
Exclude Category from Blog
<?php
add_action( 'pre_get_posts', 'be_exclude_category_from_blog' );/** * Exclude Category from Blog * * @author Bill Erickson * @link http://www.billerickson.net/customize-the-wordpress-query/ * @param object $query data * */function be_exclude_category_from_blog( $query ) {
if( $query->is_main_query() && $query->is_home() ) { $query->set( 'cat', '-4' ); }
}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 this category.
If we were using WordPress 3.2 or earlier, the code could look like this:
<?php
add_action( 'pre_get_posts', 'be_exclude_category_from_blog' );/** * Exclude Category from Blog * * @author Bill Erickson * @link http://www.billerickson.net/customize-the-wordpress-query/ * @param object $query data * */function be_exclude_category_from_blog( $query ) {
global $wp_the_query; if( $wp_the_query === $query && $query->is_home() ) { $query->set( 'cat', '-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
<?php
add_action( 'pre_get_posts', 'be_change_event_posts_per_page' );/** * Change Posts Per Page for Event Archive * * @author Bill Erickson * @link http://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' ); }
}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 (so not ones that have ended), 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.
Creating the metabox and the fields within it are beyond the scope of this tutorial, but take a look at my post on Custom Metaboxes and this Events Plugin if you you want more informaton.
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
<?php
/** * 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() && !is_admin() && 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' );


Awesome post, Bill, and most serendipitous in the timing. I’d just started going through the Codex on the WordPress query and this gave me a great leg up. Thanks very much.
Useful post, Bill – thanks! I really like your Gist implementation within this blog. I just started using GitHub myself recently and like it more very single day. It’s irrelevant question, but if you don’t mind, what are the benefits of using Gist for code snippet examples that you’re using within the blog? Are you re-using snippets later or collaborating via Gist on them? I’m just curious about usage cases. Thanks!
I do plan to reuse them. I’m on the StudioPress support forums a lot and when someone is asking how to do a specific change I’ll post a link directly to the code rather than the whole post.
But mainly I’ve found it’s easier to maintain and control the code if it’s embedded using Gist than directly added to the post. When I change up my theme I have to go through and make sure all my past code snippets didn’t break (and they have quite a few times).
Thanks for the insight, Bill!
This post would be the perfect place to show off how easy it is to run a simple custom query with Genesis!
Actually, the ‘query_args’ custom field that StudioPress recommends you use also falls under my “Don’t do this” recommendation. When you use it, Genesis is adding query_posts() to it. So if you use the blog page template and put ‘posts_per_page=5′ in the ‘query_args’ custom field, your pagination will be messed up for the reason I describe above.
Even on Genesis I recommend you use the approach detailed above. If you’re modifying the main query (which is what you’re doing if you’re using the ‘query_args’ custom field), you should do it using the pre_get_posts hook.
Thanks Bill! I’ve recently written a post on my blog (harriswebsolutions.co.uk/blog/2011/create-your-own-archive-page-with-wordpress/) on a very similar theme. But I’ve altered the query directly with SQL statements rather than using the $wp_the_query; object. Your method is much cleaner I feel. How would (is it even possible?) to use it to access other tables though (say, from a plug-in)?
You’ll want to use the $wpdb class: http://codex.wordpress.org/Class_Reference/wpdb
thanks for the tutorial. I used it to filter posts by a custom field value with BETWEEN to allow searching within a numeric range.
Bill – just reaching out here in case you have time to share some advice. I’m not that familiar with the WP Query big picture.
I have a need to have sticky posts for categories and to _not_ have sticky posts for the home page. Basically opposite of what the query.php does.
In WP 3.2.1, I was just able to change it from “if( $this->is_home && …” to “if( !(this->is_home) && ….” and it worked like I expected.
When I upgraded to WP 3.3, that simple change no longer works. It causes single post pages to fail (actually just shows the same post no matter what pageid i am trying).
I’m guessing the three other conditions on this line (line 2692 in query.php) don’t help indicate if it’s a category list vs a single post, and it was dumb luck to work in WP 3.2.1.
I’d love to use a cleaner method to do this with add_action or add_filter, or whatever else, but I don’t understand the constructs very well. I also wonder if they will even work, because the query is coded to do the is_home check, so i’m not sure how a filter/action would change that.
I’m hoping to not have to rewrite the entire query.php in my template, but that seems to be what I’ll need to do.
Any advice is greatly appreciated.
To be honest, I don’t have much experience with sticky posts and wouldn’t use them. They’re a relic from an old version of WordPress, and the functionality you’re looking for can be better implemented with post meta.
1. Add a metabox to the edit post screen that has a “Category Featured” checkbox.
2. On category.php, add a custom loop at the top that lists the category featured posts. Your query might be something like this: https://gist.github.com/1519651
3. In functions.php, exclude featured posts from the main query on category pages. Something like this: https://gist.github.com/1519659
Note that none of the code above is tested, I just typed it real quick in GitHub. It might not work, but should point you in the right direction.
Thanks for the pointers!! I’m working on implementing this now. This is something I want to be theme independent, so I’m trying to combine your custom metabox idea (which is made to add to a theme) with your custom function (core functionality) plug-in idea.
Being new to this, I’m not 100% sure how to combine them. Any tips there?
If you take a look at my core functionality plugin, you’ll see that it already has metaboxes built-in. You just have to uncomment one line in plugin.php, then create your metaboxes in /lib/functions/metaboxes.php.
I noticed that as I started digging in – good stuff, thank you! I spent 2 hours trying to debug why it wouldn’t work only to discover I had to change ‘pages’ => array(‘pages’) to ‘pages’ => array(‘post’).
I haven’t got the page/category loop implemented yet, but thanks to your code mock up, I’m on my way. Thanks for your help!
Just wanted to follow up and say thanks again. I was able to get the double loop working in my theme’s archive.php.
For the first loop, I used array_merge to keep the current wp_query args, and then added the argument to check for the category featured metabox setting i added using your custom function code.
For the second loop, i actually had to loop through the results of the first query and create an array of post id’s and then used that to pass as a post__not_in argument to a new wp_query.
Works great- thanks for the help!
Is it possible to order posts by the child term on the parent term index template with this method?
I don’t believe so. If you look at the WP_Query page in the codex, you’ll see all the ways you can order the results.
I think you’ll have to do a custom loop for that. In your taxonomy template, check to see if the current tax term has any children. If it does, loop through each child and pull those posts: https://gist.github.com/1597218
Note that this is very inefficient (there’s a lot of extra queries going on in there). Another approach could be to do a preliminary loop through the posts, storing each post object in an array and organized by tax term. Then sort that array by the tax term, and use the results to actually display the posts.
thanks Bill, it seems your solution is the same as the one I found here:
http://wpquestions.com/question/show/id/926
with the advantage that you’re using WP_Query instead of query_posts, so I will modify it to use WP_Query.
And I agree that getting the dataset first would be better to avoid multiple queries. I did something similar recently for an archives widget that grouped by year then month. Maybe I can merge all these ideas together
Thanks – helped me out with the pagination problem where pg 2 on custom taxonomies shows a 404 unless you set the backend # of posts to less than the # you’re showing using the custom query on the page.
thanks
Thanks you for the tips!
I have a question tho — is it possible to make “Customize Event Query using Post Meta”-snippet to play well with calendar? I have tried to change the code but it doesnt seem to work
The calendar makes URL for future ie. something.com/archive/2012/05. All i get is 404, even tho there are events in april
Is it possible?
Thank you!
Based on your URL it looks like your calendar is using the post date, not a separate field in post meta. WordPress will only show posts that have been published already (so have a post date in the past).
Create a custom metabox and create a field for event date (use the text_date_timestamp field type so it is a UNIX timestamp). Then you can use the event query code.
Thanks for reply.
The calendar generates link based on custom date — if in april there are events, it gives april’s link. Now when you go to that aadress, it checks posts — there are none, and it gives 404.
if ( $query->is_main_query() && !is_admin() && is_post_type_archive( ‘event’ ) ) { <– this is never true because is_post_type_archive( 'event' ) is never true. :S
how would you set the posts per page for an else?
i set posts per page for is_search, on the search.php template the else shows 10 posts with random, was hoping to avoid doing query_posts on the search.php if it can be done via pre_get_posts
Since that’s a secondary query (the primary is the actual search query), I’d build it using WP_Query. Something like this: https://gist.github.com/2719520 (untested)
ya thats what i have currently a wp query
but only
have_posts() ) : $notfound->the_post() ?>
https://gist.github.com/2719578
sorry bout that
Looks good to me, only thing that’s missing is the ; at the end of the_post();