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

    Thank you Bill for your blog and your work 🙂 and thanks in advance for your help.

    So I have a problem because my “primary-menu” disappears since I have pasted “pre_get_posts” in my Functions.php file.

    I need this “pre_get_posts” to sort my post by Deadline’s ACF fields.

    So I created others menus (header and extra menu), so It works with these menu If I remove my pre_get_post functions.

    Perhaps (probably) I have made a mistake…. Well, Bill or someone else if you could explain it to me, thanks a lot !

    see code here : https://gist.github.com/anonymous/237edb9ccb8af05b9e27

    • Bill Erickson says

      Your code is backwards. You’re modifying every query EXCEPT the main query, when you should be modifying ONLY the main query. Here’s an updated version: https://gist.github.com/billerickson/4e485f451e980a21b23f

      The conditional is saying “Only modify the main query, and don’t do it if we’re in the backend, and only modify it for the blog home or other archive pages.

      You can change the is_home() || is_archive() to apply to whatever context you want, but that should cover the post listings. You could also add is_search() but if you don’t have the ‘deadline’ field on pages then none of them will appear in search results.

      • Laurent says

        Yep ! Thank you very much Bill, You’re great !!! It works : my pre_get _post functions runs and my menu appears….
        Me, I’m so stupid because the github code I paste for you yesterday was wrong ! !
        See here the real code I had before…
        https://gist.github.com/lorangr/4c3f971fe7d0210ed7c6
        With this code I had, in all cases (home, archive, search, catégory) my posts sort by deadline.
        If you wanna tell mel why this code was wronng…..,
        Nevertheless as you told me , I have added “is_search()” Everything is allright !!
        A very very big thanks from France !!! 🙂

        • Bill Erickson says

          The issue with that code is you’re modifying every query EXCEPT those in the admin area. You still need to check $query->is_main_query() to make sure this is the main query on the page, and not a different query like the query for menu items.

  2. Alistair says

    Hi I am doing this tutorial https://sridharkatakam.com/full-screen-portfolio-with-pagepiling-in-genesis/ which is great and works fine except I would like it to show all the pages in a category before it moves on to the next page so Portfolio1 might have 7 posts Portfolio2 3 and Portfolio3 6pages how would i alter the below to achieve this ( if possible)

    function sk_change_portfolio_posts_per_page( $query ) {

    if( $query->is_main_query() && !is_admin() && is_post_type_archive( ‘portfolio’ ) ) {
    $query->set( ‘posts_per_page’, ‘5’);
    }

    • Bill Erickson says

      You won’t be able to do that with the post type archive because the posts won’t be in the correct order.

      What you should do is:
      1) Use WP Term Order to specify an order for your portfolio categories.
      2) Set the portfolio category archive query to display all posts (something high like 999).
      3) On the portfolio category archive, use get_terms( 'portfolio-category' ); (change that to your actual taxonomy name) to get a list of all the categories in order. Loop through them to find which one matches the current archive page. Use the previous and next categories as you pagination links.

      That way you start with /portfolio-category/portfolio1, then click “Next” to get to /portfolio-category/portfolio2. All the posts in that category display because you have posts_per_page set to 999.

    • Alistair says

      Hi Bill me again, I have tried a few modifications as I didn’t like the product-category being in the slug which the portfolio plugin added so i created my own CPT call the same but changed the slug to products instead. but now I have broken it as its not calling the portfolio-archive.php so I dont have individual groups anymore ie if i go to http://iwsandbox.co.uk/ultraframe/product/conservatories/ it shows the full post but if i go to http://iwsandbox.co.uk/ultraframe/product/ it shows all the posts below is my code can you please tell me what I have done wrong: https://gist.github.com/billerickson/778f2e04672b0aa97f76886c77cfdb35

      • Bill Erickson says

        Please post code as a gist since it usually gets messed up in the comment area (I updated your comment for you).

        The slug used for the taxonomy term archive is specified when you register_taxonomy(), and none of the code you provided registered the taxonomy. You’re actually adding the default WP taxonomies (category and post_tag) to your portfolio rather than the custom taxonomies referenced later (portfolio_category and portfolio_tag). You should either:
        a) Register the portfolio_category and portfolio_tag taxonomies
        b) Use the default category/tag taxonomies on your portfolio by changing the code in be_portfolio_template() to apply if is_category() || is_tag(). Although if you are using posts then this will mess up those archives as well, which is why we used custom taxonomies.

  3. Antuan says

    there any way to get exclude post dated the previous day where they exit the home chronologically? What I want is for the current day publications appear every day, and those who have passed the previous day are not even remain on file. Many thanks.

  4. Jamie Grove says

    Excellent post Bill, is it possible to use pre get posts to exclude duplicates, what I am needing to is on a taxonomy page (woo product tag) I need to exclude posts /products with the same post meta, I know how to do it via a foreach and while loop but not sure if is possible with pre_get_posts, any insight would be most appreciated 🙂

    • Bill Erickson says

      You can customize the query so that only posts with or without a certain meta value are included, but I don’t believe you can exclude posts that have duplicate metadata.

      • Jamie Grove says

        Ok thanks for getting back to me, that is what I was thinking but wanted to check that I wasn’t missing anything obvious.
        Thanks again

  5. Mazepress says

    Is there a way of creating a shortcode that can run wp reset query before your new shortcode runs? To avoid the loop affecting the query within another shortcode.

    Example: Trying to add shortcode to display 5 products of a woocommerce sub category on the parent category intro but finding the parent category loop injects within the shortcodes. Maybe off topic but can’t find anything on the subject.

    • Bill Erickson says

      You shouldn’t need a separate shortcode. The shortcode that does the query should use `wp_reset_postdata()` when the loop is complete.

      Instead of creating your own shortcode, you might try using Display Posts Shortcode. Or if you do build your own shortcode, you can use that plugin as a guide (see here for how the post data is reset).

      • Mazepress says

        Thanks for your insight Bill.

        I was using the WooCommerce provided shortcodes to display the products. These are hooked in where the product description would normally be. The full product category loop then runs underneath but for some reason all of the woocommerce shortcodes to display 5 products from specific sub categories all end up getting overwritten by the loop/query running on the product category archive itself.

        I also tried a custom module approach to display these same items but the same result happened where the query or loop for the specific product archive overwrites the intro sub category lists of products. I can’t share the link to demonstrate here as it is sensitive and while I don’t want to take up your precious time if you have a moment I could email you the link so you can see what I mean.

        Using your display posts shortcode might be the only option but wanted to see if I could get the standard built in tools to work before exploring additional tools to make it work. I was wondering if I could set a function which applies a wp_reset_postdata() every time it sees a specific shortcode in the content of a page as another workaround.

        I am using Genesis but I am also using Beaver Builder to leverage end user ease of access etc. I have them working in complete harmony though so don’t see why it would affect this.

        Greatly appreciate your input, Bill.

        Thanks

        • Bill Erickson says

          I don’t use WooCommerce (I don’t build ecommerce sites), so I’m not much help there, but I wouldn’t think the core WC shortcodes would cause query issues. I recommend you reach out to a developer that specializes in WooCommerce like Daniel Espinoza.

          • Mazepress says

            Thank for the recommendation. I will do that. Wow if you worked on eCommerce you would be lethal! I mean, you are lethal anyway Bill, everyone knows that but I am maybe a little surprised you haven’t ventured into online retail.

            Thanks again, have a nice day.

  6. Jamie Mitchell Design says

    Hi Bill

    using the pre_get_posts

    would I be able to set a taxonomy of ‘region’ to only show parent posts and not child posts?

    I’m using Hierarchical CPT’s and ALL posts show when viewing a taxonomy term

    for the life of me can’t work out how to only show the parent posts

    any advice appreciated, thanks in advance

  7. Alexander says

    Thanks for this amazing post. You can add a rewrite rule for custom post type by adding this code

    function prefix_news_rewrite_rule() {
    add_rewrite_rule( ‘news/([^/]+)/photos’, ‘index.php?news=$matches[1]&photos=yes’, ‘top’ );
    add_rewrite_rule( ‘movie/([^/]+)/videos’, ‘index.php?news=$matches[1]&videos=yes’, ‘top’ );
    }

    add_action( ‘init’, ‘prefix_news_rewrite_rule’ );

    Reference: https://www.wpblog.com/wordpress-url-rewrite-for-custom-page-template/

  8. MrO says

    Hi Bill,

    Just wanted to post a thank you here for your ‘Order results by multiple meta keys’ code snippet, it was the exact solution to my problem as well as help me understand why it works, which cannot be said of the other solutions/hacks I found on the interweb.

    FYI, the link on that page to this page is broken.

    Thanks again and happy new year!

  9. Dave says

    My site is using the latest version of WP and a Genesis theme. I tried to use your code for “Exclude Category from Blog” in the functions.php file and got this error:

    Your PHP code changes were rolled back due to an error on line 525 of file wp-content/themes/showcase-pro/functions.php. Please fix and try saving again.

    syntax error, unexpected ‘’’ (T_STRING), expecting ‘,’ or ‘)’

    FYI: Line 525 was this $query->set( ‘cat’, ‘-11’ );

    New to coding so any help is appreciated!

    • Bill Erickson says

      Everything looks right to me. Make sure you are using single quotes ' and not backticks `