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

    Thanks you for the tips!

    I have a question tho — is it possible to make “Customize Event Query using Post Meta”-snippet to play well with calendar? I have tried to change the code but it doesnt seem to work 🙁

    The calendar makes URL for future ie. something.com/archive/2012/05. All i get is 404, even tho there are events in april 🙁

    Is it possible?

    Thank you!

    • Bill Erickson says

      Based on your URL it looks like your calendar is using the post date, not a separate field in post meta. WordPress will only show posts that have been published already (so have a post date in the past).

      Create a custom metabox and create a field for event date (use the text_date_timestamp field type so it is a UNIX timestamp). Then you can use the event query code.

      • fallenboy says

        Thanks for reply.

        The calendar generates link based on custom date — if in april there are events, it gives april’s link. Now when you go to that aadress, it checks posts — there are none, and it gives 404.

        if ( $query->is_main_query() && !is_admin() && is_post_type_archive( ‘event’ ) ) { <– this is never true because is_post_type_archive( 'event' ) is never true. :S

  2. chrismccoy says

    how would you set the posts per page for an else?

    i set posts per page for is_search, on the search.php template the else shows 10 posts with random, was hoping to avoid doing query_posts on the search.php if it can be done via pre_get_posts

    • Bill Erickson says

      Looks good to me, only thing that’s missing is the ; at the end of the_post();

  3. Tyy says

    I am build­ing a direc­tory via Word­Press and Stu­dio­Press and would like to know if I can use the query_arg method via cus­tom fields to pull posts/pages that are category1 and category2? After reading your post I’m starting to think this might not be the best method for what I need to do.

    I have cre­ated a cus­tom post type of Direc­tory and then have set-up mul­ti­ple tax­onomies with par­ent and child such as:

    Loca­tions — Par­ent
    City1 — Child
    City2 — Child
    City3 — Child

    Restau­rants — Par­ent
    Amer­i­can — Child
    Mex­i­can — Child
    Chi­nese — Child
    etc, etc, etc…

    I need to be able to have a page that will pull in all restau­rants that are in City1. Can I do it using the query_arg method and if not or not recommended, what is the best method to do this? I have to do this for a lot of pages and I’m try­ing to find a sim­ple solu­tion with­out hav­ing to build a page tem­plate for every sin­gle category. Can you help set me on the right path here so I do it right the first time?

    Any help you could pro­vide would be awe­some! Thanks!

    • Bill Erickson says

      When you say “multiple taxonomies”, do you really mean you’ve created multiple taxonomies, or that you have a single taxonomy with multiple terms?

      If you create a taxonomy for Location and a taxonomy for Restaurants, then you can automatically view the posts matching a specific term using the taxonomy archive.

      If taxonomy = ‘location’ and term = ‘city-1’, the URL for the archive is yoursite.com/location/city-1. There’s no need to create a page – WordPress does this for you.

      As an example, see the code snippets section of my site. The post type archive is here ( https://www.billerickson.net/code/ ). I have a taxonomy called ‘code tags’, and here’s a term inside that taxonomy: https://www.billerickson.net/code-tag/metabox/

      • Tyy says

        Hey Bill,

        Thanks for the reply. I think we may be onto something here, but I need to figure out how to pull a page that has all the restaurants in City1 as an example. If I were to give you the login to my site, would you be willing to look at it real quick to see my set-up and let me know if I am heading in the right direction with Custom Post Types and Taxonomies? If so, let me know how to get it to you and I will send it asap. Thanks for your help with this.

        • Bill Erickson says

          I wish I could help but I don’t have any time to do free consultations (even though it should only take a few minutes). I have about 15 active projects right now and am hopping on a 5am flight to a WordCamp tomorrow morning.

          If you’d like, you can schedule to work with me in a few weeks. As of right now, I’m scheduling projects to start Monday, July 2nd.

          • Tyy says

            Hey Bill,

            I respect your time and thank you for the help you have given thus far. If I can’t get this figured out here soon I might just take you up on your offer.

            Have a great time at WordCamp!

  4. monti says

    Sorry, Bill. I’m new to commenting and did not realize I could not paste a php function in the input field here. I may be out of luck with this. 🙁

  5. monti says

    Hi Bill!

    Thank you so much for getting back to me. I really appreciate it. I pasted the code like you said at the following URL:

    git clone git://gist.github.com/2883980.git gist-2883980

    The address of my site is: http://www.serv-hot.com but the site is in maintenance mode so you will need to login with the following credentials to take a look:

    USERNAME: admin
    PASSWORD: 3000hot

    (you will see the tiny ‘login’ button on the bottom right-hand side of the screen of the maintenance mode page.)

    Once inside you can navigate to the Appearance editor where you must choose the SIGHT theme from the pulldown menu above the list of template files. This is the theme that I’m using; not the default theme TWENTYELEVEN.

    Any insight you can provide on this issue will be a godsend. Thank you. Hope to hear from you soon. — monti

    • Bill Erickson says

      Can you post just the specific code you have a question about, and the question itself?

      Logging into your site and troubleshooting is beyond the free help I provide on my site, but if you’d like to hire me feel free to use my contact form: https://www.billerickson.net/contact

  6. Julio Alarcon says

    Bill, I used your first code under “Exclude Category from Blog” including it inside of home.php. It didn’t work. I tried several positions, in one of them it turns me the site completely blank.
    Can you give me your opinion to get me in the right direction. ?
    My site uses the Genesis Design Framework from StudioPress Version: 1.8.2 in a WordPress 3.4.1 platform.
    Thank you Bill & congratulations for you good job.
    Julio

    • Bill Erickson says

      That’s actually a very common problem, which I addressed about halfway down the page in bold.

      You must place your function in functions.php (or a plugin). WordPress needs to build the query to figure out what template to load, so if you put this in a template file like home.php it will be too late.

      • julio alarcon says

        Bill, I put the code in functions.php but it didn’t work .

        I think I have a BIG confusion. When this article starts you say “Some examples: Don’t display posts from Category X on the homepage.” I thought, that is what I want to do. On my WordPress Reading Settings I have My Front page displays>> Your latest post set. So my Homepage shows all my posts from the more recent in the top of the page. I’m not using the Blog Template in other words.

        MY CONFUSION is when you write about Exclude Category from Blog and you give the first code for copying and pasting it. Is that code exclusive only when you are using the Blog template ? Let’s say Settings>>Reading>>Front page displays>>A statis page>>Front page: Blog

        Is it a different code when y have my homepage not set as a Blog, only the default homepage?

        Sorry the mess, I can not explain it in other words.

        Thank you

        • Bill Erickson says

          The is_home() parameter will return true only on the blog. This will be on the front page if you have your blog on the front page, or on some other page if you have the blog set to some other page.

          So this code should be working for you. Have you tried switching to the default WordPress theme and doing this code to see if it’s an issue with your theme? Your theme might be modifying the query incorrectly which is messing up your code.

          • julio alarcon says

            I switched to the default WordPress theme Twenty Ten. Then I went to Theme Functions
            (functions.php), I went all the way to the bottom and pasted there your code on…(Exclude Category from Blog). And it showed all the posts excluding the ones corresponding to the category I inserted in your code ( ‘cat’, ‘-115’ ). I tested with others categories and it worked well.
            So which would it be the problem with the theme I want to use, Eleven40 Child theme ?
            Thank you Bill

  7. Bill Erickson says

    Julio, this is most likely because of the genesis grid loop. I’ve had issues with it since it creates a new query in the page rather than using the main one.

    You can open home.php and place your query arguments directly in the genesis_grid_loop() function, but you’ll get 404 errors when you get to the last of your blog posts (you’ll actually be getting 404 errors right now because of eleven40’s ‘posts_per_page’ => 5 argument).

    A better approach would be to rebuild the homepage to use the main WordPress query.

    • julio alarcon says

      Hmmmm, I do not want to be up to my neck in mud.
      Ok, I want to determine how to show posts of a specified category in my homepage. It can be one specified category or two specified categories, etc, and filter or block the other categories, that’s my goal.
      As you publicize in your article…”Don’t display posts from Category X on the homepage”, that’s good, but it doesn’t work with my eleven40 child theme, I copied the code you obsequiously give us under “Exclude Category from Blog” and pasted it inside of function.php and it didn’t work.
      Bill when you write “You must place your function in functions.php (or a plugin)….” Which is that Plug-in ? Do you think that Plug-in could solve my problem instead of trying different codes in function.php?
      Is that Plug-in the one you present in “Also take a look at my Display Posts Shortcode plugin, ….”?
      Thanks Bill

      • Bill Erickson says

        The proper method outlined above will not work with your theme unless you remove the genesis_grid_loop() function from your home.php file.

        Alternatively, you can use an improper method by modifying the genesis_grid_loop like this:
        Actual code: https://gist.github.com/3060649
        Difference: http://diffchecker.com/YVeR7tmA

        Change ‘homepage-posts’ to the category slug you want displayed on your homepage.

  8. Wes Linda says

    So i’m attempting to figure out how to offset posts on the homepage of the new education theme from StudioPress. I’ve read the tutorial and I guess I’m simply unsure of how to do this. I’ve tried numerous ways shown on a number of sites, with no success. Any ideas?