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

    Hi Bill & Congrats for the excellent blog.

    It helped me a lot, as i’m taking my first steps on genesis and the way you describe things is really simple, comprehensible and to the point!

    I would like to ask for some help, as iterating through all of these comments didn’t provide me with a solution.

    I have implemented two loops in the home page of my website. The first one is a custom loop, showing the 4 latest posts of the same post category, for example “X”. The second one is implemented with genesis standar loop, showing the latest post οf all categories, including “X”. This loop is using pagination, so users can turn to page 2, 3 etc.

    Is there a way to exclude only the 4 posts of “X” category appearing to the first loop, from the second one? Probably via a function combining category and offset?

    Thanks in advance!

    • Bill Erickson says

      If they were two custom queries, then you could exclude the first four posts by adding 'post__not_in' => $posts in your second query, where $posts is an array of Post IDs.

      Doing this with the main query is more difficult. You’ll need to hook into pre_get_posts to exclude those four posts from every homepage query so the pagination works.

      I haven’t tried it, but something like this in functions.php could do it: https://gist.github.com/billerickson/7cba51537b090b966f00c3f690d65f6a

      What we typically do though is make the homepage a static front page with a bunch of custom queries, then have “More Posts” buttons that link to the blog page or category archives. See Lil Luna as an example.

      This way we can feature whatever content makes the most sense (and change it seasonally) without affecting which posts appear in the main blog.

  2. Jairam Swami says

    Hii,

    I need to customize the product category archive template. I want to show parent category products in a child category archive template.

    For details lets assume these categories:-

    All
    business (child of All)
    Work from home (child of business)
    online business (child of business)

    Current if we open the archive for “All” then all post form child category (business, Work from home, online business ) will show and if we open the archive for “Business” then all post form child category (Work from home, online business) will show. And this default functionality of wordpress or woocommerce. But I need that if archive for “online business” (child category) opened

    then show products from this selected category (online business).
    also show post from all parent categories (All,Business) of this category.

  3. Stephen Tapp says

    Hi Bill,

    I’m using a query that filters posts containing featured images. (I have no problem doing that with: ‘key’ => ‘_thumbnail_id’, ‘compare’ => ‘EXISTS’) But I would also like to filter those posts with featured images > 600px wide. I can’t find any reference to such a key. Do you know if one exists for featured image width and/or height? LMK if you need more info. Thanks!

    • Bill Erickson says

      Unfortunately no, WordPress doesn’t store the dimensions of the featured image as metadata. You would need to start storing that information yourself.

      It’s a two-step process:
      1. Start saving the data for new/updated posts. Hook into save_post, then use $image = wp_get_attachment_image_src( get_post_thumbnail_id(), 'full' ); to get an array of image dimensions (width = $image[1]).
      2. Bulk update existing content. This would probably be best done with a custom wp cli script. If you don’t have too many posts, you could run the query directly in the page and hope it doesn’t time out (example). If you have lots of posts, I would paginate it, doing 100 at a time until there are no remaining (example).

      I hope that helps.

  4. Scot MacDonald says

    Bill

    Really appreciate your tutorials and plugins.

    I’m trying to configure a custom loop in Genesis that displays three posts by date with a heading above displaying the current date. A good example (though not WordPress) can be seen at Sidebar.io.

    Any assistance you could provide is greatly appreciated.

    Thanks

  5. Adriana says

    Is there a way to display a specific number of posts on page 1 vs the other pages of the blog?
    My site is NOT setup to have a static homepage, so the homepage is the blogs’s page #1. So in my homepage I have it set to show 3 posts with the pre_get_posts function, but I want to display more posts on page 2, 3 and so on.

    Is there a way to make this happen?
    Set page/2/, page/3/ and so on to display say 9 posts as opposed to page 1 showing only 3 posts? Thanks!

    I’m currently using this:

    function change_number_posts_frontpage( $query ) {
    if( $query->is_main_query() && ! is_admin() && $query->is_front_page() ) {
    $query->set( ‘posts_per_page’, ‘3’ );
    }
    }
    add_action( ‘pre_get_posts’, ‘change_number_posts_frontpage’ );

      • Adriana says

        Thank you so much Bill! Your code worked perfectly.
        There’s one little problem though…
        Now my ‘Flexible Post Widget’ is also only showing 3 posts and I had it set up to show 4 posts.
        Is there a way to have the script not affect the genesis Flexible Post Widget?
        Thank you again!

  6. Adriana says

    I forgot to mention this is also affecting my search results, showing also 3 posts on the first page.

  7. Renan says

    Hi Bill! Excellent post. The problem is that in my case it is not showing the posts, but only the home page. Below the code and print.

    ** Functions **

    function tax_and_offset_homepage ($ query) {
    if (! is_admin () && $ query-> is_home () && $ query-> is_main_query ()) {
    $ query-> set (‘post_type’, ‘post’);
    $ ppp = get_option (‘posts_per_page’);
    $ offset = 6;

    if (! $ query-> is_paged ()) {
    $ query-> set (‘posts_per_page’, $ offset + $ ppp);
    } else {
    $ offset = $ offset + (($ query-> query_vars [‘paged’] – 1) * $ ppp);
    $ query-> set (‘posts_per_page’, $ ppp);
    $ query-> set (‘offset’, $ offset);
    }
    }
    }
    add_action (‘pre_get_posts’, ‘tax_and_offset_homepage’);

    function homepage_offset_pagination ($ found_posts, $ query) {
    $ offset = 6;

    if ($ query-> is_home () && $ query-> is_main_query ()) {
    $ found_posts = $ found_posts – $ offset;
    }
    return $ found_posts;
    }
    add_filter (‘found_posts’, ‘homepage_offset_pagination’, 10, 2);

    ** Homepage **

    ** Print **
    https://imgur.com/WyCzuNY

    • Bill Erickson says

      In Settings > Reading, do you have it set to show posts on the homepage or a static homepage? Based on your screenshot I think you have it set to show a static homepage using the page “Home”. You should change that to “Show the latest posts”.

  8. Alston says

    Hello Bill,
    Your wp_query args post is bookmarked! How would you approach adjusting the output of a custom wp_query that pulls in all of post_type => attachment (images) that are attached to a custom taxonomy specific to a user. My query works correctly to return all images uploaded and attached to that user’s custom taxonomy.

    My issue is each image also has another custom taxonomy like “fruits” where if the image is a banana it’s tagged to the custom tax of “banana” in addition to the users taxonomy. So this query that returns all of a users images I’d like to output into sections based on their tax of fruit. So section one might output all apple images from the query, section two outputs all banana images from the query, etc.

    Everything I find leads me to pre_get_posts but I don’t think it’s what I need here based on the initial custom wp_query..?

    • Bill Erickson says

      I would run the initial query to gather all of the images. I’d then create an array for breaking down the images by the second taxonomy. Something like:

      $images = [];
      $loop = new WP_Query( $args ); // your query to get all the images
      while( $loop->the_post() ): $loop->the_post();
      	$terms = get_the_terms( get_the_ID(), 'fruit' );
      	foreach( $terms as $term ) {
      		$images[ $term->slug ][] = get_the_ID();
      	}
      endwhile;
      wp_reset_postdata();
      

      You could then loop through the $images array to display the images broken down by specific terms.