Customizing the WordPress Query

santorini

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' );

Bill Erickson is a WordPress Consultant who builds custom websites using WordPress as a CMS and the Genesis framework. He contributes to the WordPress community through free themes, plugins, tutorials, and core patches. He's also a cofounder of the BIL Conference (the open analog to the TED Conference).

Looking for more great tutorials? See them all!

Comments

  1. Dan Tudor says:

    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.

  2. Max says:

    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!

    • Bill Erickson says:

      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).

  3. Jacques says:

    This post would be the perfect place to show off how easy it is to run a simple custom query with Genesis!

    • Bill Erickson says:

      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.

  4. 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)?

  5. Paul says:

    thanks for the tutorial. I used it to filter posts by a custom field value with BETWEEN to allow searching within a numeric range.

  6. cori says:

    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.

    • Bill Erickson says:

      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.

      • cori says:

        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?

        • Bill Erickson says:

          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.

          • cori says:

            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!

  7. cori says:

    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!

  8. Paul says:

    Is it possible to order posts by the child term on the parent term index template with this method?

    • Bill Erickson says:

      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.

      • Paul says:

        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 :)

  9. 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.

  10. charlie says:

    thanks

  11. fallenboy says:

    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!

    • Bill Erickson says:

      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.

      • fallenboy says:

        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

  12. chrismccoy says:

    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

  13. chrismccoy says:

Speak Your Mind

*

If you'd like to include code in your post, please post it to http://gist.github.com and include a link.