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. Gena says

    Quick question.

    How do I exclude Genesis blog template from this grid loop.

    I can see this if( ! ( $query->is_home() || $query->is_archive() ) ) and from codex it says the “pre_get_posts action doesn’t work for Page requests.”

    • Bill Erickson says

      You cannot exclude the Genesis blog template. Do not use the Genesis blog template. A blog post is coming soon that goes into more detail.

  2. Aaron says

    Hi Bill,

    Thanks for the great tutorial.

    I’d like to sort a CPT Archive dynamically and wonder if pre_get_posts can be used in conjunction with a query string to accomplish this.

    Here’s a quick example, but I haven’t been able to get it to work: https://gist.github.com/aaroncolon/9244757

    Guessing it’s not possible or I’m missing something very obvious. Any help is greatly appreciated.

    Thanks,
    Aaron

    • Bill Erickson says

      That code was pretty close. I’ve made some changes to it: https://gist.github.com/billerickson/9250280

      Basically you were doing isset( $_GET[‘sort’] ) == ‘something’. isset() tells you is that variable “is set”. It will return true or false. So I modified your code to see if it was set, and then to see if it matched your criteria.

      • Aaron says

        Of course, that makes sense. Your updated code works great.

        I noticed you changed:
        is_post_type_archive( 'films' )
        to:
        $query->is_post_type_archive( 'films' )

        What is the reason for this change?

        Thanks again,
        Aaron

        • Bill Erickson says

          As long as you’re running well-written code on your site it won’t change anything, but it’s good practice.

          pre_get_posts is passing us a $query object, so we should be checking it for our conditionals rather than the global $wp_query.

          is_post_type_archive() is really just a wrapper for global $wp_query; $wp_query->is_post_type_archive() (see here).

          The only time they would be different is if a theme or plugin is using query_posts to modify the query.

    • craig says

      aaron,
      i noticed you deleted the gist from your oringial question. would you mind posting it again, i wanted to see the whole thing.

  3. Kevin says

    Is it possible to query posts based on the current date and then randomise the results using ‘&orderby=rand’.
    First I need to query the posts based on the current date and then apply ‘&orderby=rand’ to it? How do I do it?

  4. Hung Pham says

    Hi,

    I want to combine the filters for all posts having publish status and all owned private posts. I am thinking to use
    – $query->set (‘post_status’, ‘publish’): for all published posts
    – $query->set(‘author’, $user_ID): for all owned posts

    But don’t know how to combine them together in one filter. Please advice.

    Thanks.

  5. Andy Jobben says

    Hi Bill
    I want to order posts from a specific categorie. So i used your last code snippet and changed
    is_post_type_archive( event’ )
    to
    is_post_type_archive( ‘wordpress-website-maken )

    But after the change my website breaks.
    I remove the code and its all good again.
    What did i do wrong?
    What can i do to sort those posts by number?

    thanx in advance.
    andy

    • Bill Erickson says

      Two things: First, you forgot the closing ‘ at the end of maken. That’s probably what broke your site.

      But more importantly, is_post_type_archive() only applies to post type archives. So if you created a custom post type called “WordPress Website Maken” then yeah, that would be the right one to use. But it sounds like it’s a category of posts. So you’ll want to use is_category()

  6. Ross says

    Hey Bill, great post! Even though it’s 3 years old, it still helped me figure out the damn query issue I was having.

    But now I’m stuck on a new one – I’ve installed the WTI like plugin so that users can rate posts and want to sort grid items on my front page by date/score. My php looks like this:

    $query->set(“orderby”, ‘date meta_value_num’);
    $query->set(“order”,”DESC”);

    As per the instructions at http://codex.wordpress.org/Class_Reference/WP_Query and http://www.webtechideas.com/sorting-posts-by-meta-key-and-value/

    Date seems to be sorting properly, but score doesn’t. My guess is that perhaps the orderby isn’t parsing properly and so it’s just defaulting the default date sort? If I just sort by score, that works fine… if I just sort by date that obviously works fine… it’s combining the two that doesn’t work. Even tried upgrading to WP 4 beta 3 to try the new multi-order des/asc feature, but no joy.

    Any thoughts? Website I’m having issues with is at http://goo.gl/TfPIzR

    • Bill Erickson says

      I don’t think the sort by date will work for you. It is actually sorting by the UNIX datetime. So the only way the second orderby parameter will be used is if two posts were published the exact same second. If they have different datetimes, then it doesn’t matter what the score is since the one with an earlier post date will show up first.

      If you sorted by ‘meta_value_num date’ it would show the highest ranked first, and if two items have the same score it would then sort by date.

      • Ross says

        Damn. Unix date makes it pretty impossible to sort by ‘date’ rather than ‘time’ unless i hack the SQL calls from WordPress.

        What a pain.

        Ah well, thanks for the quick response Bill.

        • Bill Erickson says

          You could add a meta_key that stores just the date. Ymd, like 20140521. Then you could sort based on that and your other meta field.

          • Ross says

            Probably a better idea – I had considered that in the wee hours last night but lack of sleep made me forget. If I can automate the population of that field, even better.

            Is it possible to sort on two meta fields?

            I’ll jump in tomorrow and give it another shot, cheers Bill

  7. Ross says

    Yeah, I actually gave the array a shot yesterday. Didn’t help with the date problem obviously.

    With the meta key, the issue is that you ‘orderby’ meta_value_num and then specify the meta_key in a subsequent setting, so I’m not sure if I can specify multiple attributes in meta_key.

    $query->set(“orderby”, ‘date meta_value_num’);
    $query->set(“meta_key”, ‘_wti_total_count’);

    Guess there’s only one way to find out 🙂

    • Ross says

      Interesting addendum, I changed some of my post dates to 00:01 and ran the same sort… STILL didn’t work.

      Looked in the database and of course the date shows up as 00:00:01:22 or 00:00:01:12 and the order by still uses

      I’ve ended up hacking query.php, which I don’t really like… but it works:
      case ‘post_date’:
      $orderby = “CAST($wpdb->posts.{$orderby} as date)”;
      break;

      I’ll have to make sure I turn off auto-updates and re-hack the query file every time I upgrade in future… I’ll see if I can make a suggestion to modify the core

      • Bill Erickson says

        Why not make this change in pre_get_posts, as described above? Then the code can be in a theme or plugin.

        • Ross says

          You can specify what attribute to sort on with pre_get_posts, but I don’t believe you can specify a cast… if the orderby attribute you set in the query doesn’t match the expected values, query.php (core) will ignore it

  8. Easy Mark says

    Hi there, Bill:

    I know this is an older post, but I just want to double check on one thing.

    In line 13 of the LAST example posted, you have an if statement that starts out like this:

    if( $query->is_main_query() & !$query->is_feed()…

    Just wasn’t sure if this was a typo and that there are supposed to be two && after the is_main_query() or whether there is only supposed to be one.

    If it is just a type, and there were supposed to be two &&, then I understand that line.

    If there is SUPPOSED TO BE only one, then I am going to need some help figuring out what the difference in php is between one & and two &&…

  9. Ian says

    Hey Bill!

    Still can’t believe how much love this post gets all these years later 🙂

    I’ve been trying to figure out how to change the loop on category pages so that I can display them in A-Z order, but also so that I can separate posts by letter (i.e. All posts starting with A, then B and so one, with jump links to each.

    I’ve got the A-Z working but can’t figure out how to customize the loop to show what I’m trying to achieve. Any pointers you have on this would be great. I’ve search, but it’s not something that comes up too often…

    Cheers!

    • Bill Erickson says

      There’s no easy way to do that. One approach is to create a new taxonomy called “Post Letters” or whatever you want to call it, then put posts that start with “A” in the “A” category. Then to see all the posts that start with A, you go to that taxonomy term archive.

      A more automated approach is to write a function that runs on ‘save_post’ to update a meta key that stores the first letter of the post. Then when you want to view posts that start with a certain letter, you do a meta_query for posts with that value as the meta_key

  10. Steve P says

    Hey Bill,

    I am trying to exclude a category from my custom post type archive but am not having any luck. I am using the genesis framework, executive pro theme and set up custom post types and archives in it.

    I also tried excluding categories form the whole site with no luck as well.

    Below is the code I put in the functions file but not sure if there is an easier code I can put in the custom post archive file.

    https://gist.github.com/billerickson/284f5b4135c67c66b3e0

    ANy help would be appreciated.