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:
- Exclude posts from a certain category on the homepage
- Increase or decrease the number of posts displayed per page for a specific post type
- Sort posts in your category archive by comment count rather than post date
- Exclude posts marked “no-index” in Yoast SEO from your on-site search results page
- 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.
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 addis_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.
Laurent says
Ok ! I’ am absolute beginner 😉
Thanks again for your disponibility.
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
Thanks for the swift reply you lost me on number 3 any help much appreciated
Regards Alistair
Alistair says
Don’t really know how but I think I have it working except the previous and next buttons I added the below from your tutorial https://www.billerickson.net/genesis-portfolio/ and that did it see http://iwsandbox.co.uk/ultraframe/portfolio_category/conservatories/
so now how would I add the previous and next buttons in
function be_portfolio_template( $template ) {
if( is_tax( array( ‘portfolio_category’, ‘portfolio_tag’ ) ) )
$template = get_query_template( ‘archive-portfolio’ );
return $template;
}
add_filter( ‘template_include’, ‘be_portfolio_template’ );
Bill Erickson says
The code you just cited is what tells WordPress to use the ‘archive-portfolio.php’ template when on a portfolio category. It has nothing to do with pagination.
In your archive-portfolio.php file, add something like this: https://gist.github.com/billerickson/154f4087ea3704451816942a15357625
Alistair says
Your a legend Bill thanks for all your help
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 ifis_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.Alistair says
Thanks Bill I have added it link below all my stuff at the bottom of the file
https://gist.github.com/theimageworks/70b37cb34659e2a53236e180446dbecc
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.
Bill Erickson says
Yes, see the Date Query arguments. Something like this: https://gist.github.com/billerickson/512d800473d32f506239aed82874d597
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
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.
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
Jamie Mitchell Design says
Hi Bill
kinda figured it out 🙂
just posting here so others can see
https://gist.github.com/jamiemitchell/bad4ba342c387fca551f7ddcef234b7c
Bill Erickson says
Looks good to me. I’d also add
$query->is_main_query()
to the conditionals to make sure it only applies to the main query.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/
Bill Erickson says
Thanks. Here’s some more information on custom rewrite rules: https://www.billerickson.net/code/custom-rewrite-rules/
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!
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`