Customizing the WordPress Query with pre_get_posts

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:

  1. Exclude posts from a certain category on the homepage
  2. Increase or decrease the number of posts displayed per page for a specific post type
  3. Sort posts in your category archive by comment count rather than post date
  4. Exclude posts marked “no-index” in Yoast SEO from your on-site search results page
  5. 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.

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

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. Stephen Harris says

    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. Wayne Burke says

    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.