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. Steve P says

    I tried the code you mentioned. archive name is business listings that I swapped out for events. The categories are still showing however on the business listings archive.

    /**
    * 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() && $query->is_post_type_archive( ‘business-listings’ ) && !is_admin() ) {
    $query->set( ‘category__not_in’, array( 12, 34, 47 ) );
    }
    }
    add_action( ‘pre_get_posts’, ‘be_exclude_category_from_blog’ );

    • Bill Erickson says

      Are you sure you have the ‘category’ taxonomy being used on this post type, and not a custom taxonomy like ‘business-category’ ? the ‘category__not_in’ only works with the taxonomy called “Category” which is applied to the “Post” post type. I’m 99% sure you’re using a custom taxonomy since that’s a more appropriate way to do i t.

      In that case, you’ll need to use a taxonomy query. Actually writing the code for you is outside the scope of free support I offer in my blog comments, but this can get you started: http://codex.wordpress.org/Class_Reference/WP_Query#Taxonomy_Parameters

  2. Steve P says

    Thank you for your response.

    taxonomy=industry&post_type=business-listings

    I will try the taxonomy queries and see how that goes.

  3. Chandra says

    Is this possible to display slider from single post uploaded images? Or else is there any plugin to upload images in post and display it as slider on single post..

    • Bill Erickson says

      Try Soliloquy. Or download my Gallery Rotator plugin for free at GitHub, upload the images, click “Create Gallery”, add them to a gallery, and before inserting the gallery into your post click “Display as rotator”.

  4. Kevin says

    Hey Bill I am trying to do the following to a blog and am having a heck of a time:

    • Customize the first post to be full width, make it more of a featured/hero
    • After the new featured/full-width page I want to just list the posts per usual (using Genesis theme). And have a sidebar. All my other blogs I do this easily, I let Genesis list out the posts and I create a custom sidebar.
    • Any archive pages would just be the default list of posts no featured post
    • Preferably compatible with Infinite Scroll via Jetpack

    Any insight at all would be great. Loving the new site fonts btw! Nice and clean.

    Thanks as always,

    Kevin

    • Bill Erickson says

      If you just wanted to change the style or output of the first post, that would be easy. For instance, use the ‘pre_get_option_image_size’ filter to modify the image size for the first post. The issue is that the first post needs to occur outside the content area.

      I’d try something like this in home.php: https://gist.github.com/billerickson/5a710b9d759da3311b43

      That’s untested, but it should get the first post set up and displayed after the header. Then when you get to the actual loop it will start with the second post, since you didn’t rewind_posts().

  5. ChrisScottUK says

    Hi Bill,
    Thanks so much for this great site, and resource!
    1 question I have – Is it possible to use a custom field on a page editor to determine which category of posts would be displayed on that page.
    I’m trying to figure it out, but am getting myself in knots. I need to allow someone editing the page to make quick changes to which categories would be displayed without amended the code each time

    • Bill Erickson says

      Yes. You could use the Custom Fields metabox (click “Screen Options” in top right corner of Edit Page screen to turn it on if it isn’t already active), or you could create a custom metabox using CMB2 or ACF.

      Give them a field where they can specify the category slug. Then in your theme file, when you’re putting together your query do something like this, where ‘be_category’ is the name of the meta field that has the category slug.

  6. Kan says

    Thank you Bill.

    After countless hours of trying to get pagination to show the correct number of pages on a new WP_Query within an custom post type archive template, using your solution to customize the main query and using a normal loop on the custom template page did the trick.

    And going through the slides for Andrew Nacin’s talk also gave me some good insight. I seriously cannot thank you enough. How can I buy you a beer?

    • Bill Erickson says

      Glad to help! A great way to “buy me a beer” virtually is to share this post on Facebook (button is below the article). The more people see it, the more likely it will send a new client my way 🙂

  7. Mehdi Madhkhan says

    Hi bill,

    Thanks a lot for this impressive post. I wouldn’t really like to bother you or anyone who would take the time and trouble to post an answer to my question, but, it seems after I’ve modified the main query this way, the codes on my
    archive.php
    file, which was provided by the theme developer won’t work anymore and nothing appears on the category (archive) page. I’ve tried hard to find the answer on WordPress Codex but most of what I see there is simply too hard for me to understand. Would you kindly let me know how to show results after the query’s been implemented?

    So sorry if this looks like a rookie’s question as I’m both new to object-based PHP coding and to WordPress coding.

    Any help would be truly appreciated.

    • Bill Erickson says

      Can you post the code you’ve used to customize your query? Go here and paste the code (so it’s formatted correctly), then grab the link and post it in a comment.

  8. Mehdi Madhkhan says

    hey Bill,

    Sorry if I’m disorganizing comments on your page but seems my previous comment was not published so I can follow up and I have to write another one.

    Earlier, I posted this comment on your website:
    Hi bill, Thanks a lot for this impressive post. I wouldn’t really like to bother you or anyone who would take the time and trouble to post an answer to my question, but, it seems after I’ve modified the main query this way, the codes on my archive.php file, which was provided by the theme […]

    And received this reply via email:
    Can you post the code you’ve used to customize your query? Go here and paste the code (so it’s formatted correctly), then grab the link and post it in a comment.

    Here’s what I’ve done so far:
    First, I add the following function from your website to my functions.php file:
    https://gist.github.com/anonymous/4ee9af8d8502871ec061

    Up to this point, posts from category with ID 64 have disappeared from the archive.php (category page) which means the function has been successfully applied to the category but I don’t know what to do next to show posts from that category ordered by their title ascending. When I try to gettype($query) on my archive.php file, it claims that $query is “Null” and I don’t know how to use something Null or to rebuild the query object. And here’s the code on my archive.php which was provided by my theme developer:
    https://gist.github.com/anonymous/9636372ade3b6d6e4758

    As you can see, I’m quite new to OOP and as such, I’d really appreciate any help (even easy to understand tutorials) so I can fix the code. Thanks a lot for your kind cooperation in this matter.

    • Bill Erickson says

      What specifically are you trying to do? Why have you excluded that category if you’re trying to list its posts ordered by title?

      • Mehdi Madhkhan says

        Thanks for your reply. Seems I’ve really F*ed up the code! As I told you, I’m quite new to WordPress programming and I’ve only worked on writing simple CMSs of my own using basic SQL queries.

        I thought first I should exclude the category I’m trying to fetch posts for and then send another query to fetch the posts ordered by title to form a list of posts. If I’m wrong (which is quite likely) would you kindly tell me what’s the practical way to do that? If I’ve failed to convey what I mean by now I’d like to say “I want a list of posts in that particular category ordered by title and with a certain number of posts on a page. Would you kindly provide the code snippets I should use on functions.php file and on archive.php file? I think it would benefit every novice user like me”. Thanks in advance.

          • Mehdi Madhkhan says

            Thank you but still WordPress outputs its default results on category 64 results page. I tried adding the line

            echo “Hi there!!!”;

            to the function to see whether it’s being called at all on category 64 page but the phrase “Hi there” appeared before any other source code on the page. Maybe that’s because it’s not in objective style. I don’t know why the default results are not being overwritten by the function. It seems I’ll have to use a specialized file called category-64.php and change the default code on it (which I don’t know how). Any updates on that?

          • Bill Erickson says

            The code I linked to above will make the category with an ID of 64 display its posts ordered by title, A-Z.

            If you need more help, I recommend hiring someone on Codeable.

          • Mehdi Madhkhan says

            Thanks a lot. You’ve been very helping. Yes I think I’ll go for that. I don’t know why I can’t see your last reply here but I hope you receive my message. Have a great time!