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.
Razvan says
Hi Bill and thanks for a great post! I was trying to change the number of posts shown in a particular category, but because of the posts_per_page pagination bug I simply couldn’t get it done, no matter what other possible solutions I’ve tried. Yours did the trick! Have a great one and thanks again!
Smythe says
Hi Bill,
Thank you for taking the time to write these posts — very helpfu!
I am new to WP & Genesis, have some coding experience with minor PERL programming 10 years ago. I am having a challenge with how posts are handled at the index (home) versus the Archives. At the index I want one post, the most recent, to show in full. On the archives, I want multiple posts to show as an excerpt. The problem is WP & Genesis settings gives me either excerpts globally and/or 1 post globally (both home and archives). At WordCamp Atlanta, Travis Smith helped me with part of the problem (removed pagination on Archives — only one post listed at a time) but I later learned it didn’t address the entire problem. It is here:
https://gist.github.com/wpsmith/5177901
I know I’m on the right track and I need some direction, even how to go about it. Any assistance would be most appreciated! Also, starting PHP tutorials this afternoon!
Thanks,
Smythe
kate says
Hello Bill,
I was using fine the new WP_Query all the time, but then was told that it causes performance issues on the big project. Was suggested to use ‘pre_get_posts’ action. I browsed the web for quite long time, tried all the possible ways to use it in my case, but no success.
In my case, every post has one of the categories (berries, veggies, fruit). Some of the posts have custom field ‘rating’ that contains some integer. I need to display the posts making sure that posts with higher rating are displayed first, and the are followed by all the rest (involves infinite scroll during it too).
Here is what I tried: https://gist.github.com/emelyanenko/ff0f74c4293d58d43bd4
Printed request looks like that:
SELECT wp_posts.* FROM wp_posts WHERE 1=1 AND wp_posts.ID = 227 AND wp_posts.post_type IN (‘post’, ‘page’) ORDER BY wp_posts.post_date DESC
And it only returns me the content of that page with id = 227.
How can I make it return me the other posts, not only the containing page? Ideally, I would need this action to be applied only to that particular query where I’m displaying the posts of the 3 mentioned above types.
Thank you in advance
Bill Erickson says
You should only use pre_get_posts if you’re customizing the current page’s query. In your case, I think a custom query will be best. You’re trying to load these results on a page, so you need the page’s query to run first and then do your custom query inside of it.
WP_Query actually does 4 queries to the database:
The first one is the important one. If you don’t need the other three, you can turn off the ones you don’t need like this: https://www.billerickson.net/code/improve-performance-of-wp_query/
It looks like you’ll need all of them. You need the # of posts for pagination / infinite scroll, you need the categories, and you care about post meta (rating).
Don’t worry so much about the performance of this query. Just make sure you have a caching plugin installed and then the query performance won’t matter much.
Toure says
I have been trying to query posts and hide all pages from it, but no success. Any help will be appreciated.
https://gist.github.com/billerickson/5670429
I am using the above code and it works fine, but it is querying also my pages which I don’t want to see.
Bill Erickson says
First, never use query_posts. Read the article above for more information on why.
Here’s an article on doing custom queries (which it looks like you’re trying to do): https://www.billerickson.net/custom-wordpress-queries/
Hung says
Hello,
Is there anyway to exclude multiple categories?
Thanks.
Bill Erickson says
Yes. See the Codex for all the options on categories: http://codex.wordpress.org/Class_Reference/WP_Query#Category_Parameters
Brian Novak says
Bill,
Is there any way write the function to simply include one category and exclude all others? I have many categories that and will be adding new ones that I do not want to appear on the homepage feed.
Thanks,
Brian
Nathaniel Ryan says
I’ve gone through these steps, but must be overlooking something very obvious, and hope you can assist. I want my category page(s) to sort A-Z.
So using your example of Events:
When I were to go to /events/ I would like the posts to be listed A-Z, as well if I have a child inside events called Sports or Free, then the A-Z sorting would still work on
/events/sports/
/events/free/
I’m using Genesis, and feel like I should be able to go into my Child theme functions.php or using the Simple hooks plugin, I should be able to say orderby ‘title’ and order ‘ASC’ but I’m not having luck so far.
Thanks so much-
Nate
Bill Erickson says
In functions.php, place this code (leave out the https://gist.github.com/billerickson/6298213
Nathaniel Ryan says
Brilliant. Your code worked perfectly. I have always admired your tutorials and Genesis work. Thanks again, and I would like to share this with a few co-workers if you are okay with that.
Antonio says
Hello Bill,
imagine you have two kind of “events” and want to use the same pre_get_post hook for both.
But then, in one event custom post type you have defined a custom taxonomy “country” so you can query by $query->set( “country”, “uk”) but then, in the other event custom post type you havent defined any country to uk but still want to have a valid query.
How would you do that?
Since we are in the main_query we still dont know if “that query” will be empty so we cannot choose when to query by country uk or query by every country.
I am doing it so far with another function.
https://gist.github.com/antorome/b76cbdae202a4d22efe3#file-gistfile1-php
In the example the tax is “seccion” and “mejores-ofertas”.
so then back in the pre_get_posts I do
if (cdb_productos_afiliacion(“event”))
$query->set(“seccion”, “mejores-ofertas”)
Do you know if there is anyway to add query->set(“country”, “uk”) directly in the main query so if the given custom post doesnt have any post with a country tax set to uk, it will query the default one???
Thanks
Bill Erickson says
I don’t understand the question. Are you saying you have two different ‘event’ post types? Why?
Why don’t you use the taxonomy archive for your country query? Go to /country/uk and you’ll see all events in that taxonomy term. Go to /events to see all events, regardless of country.
Dan R. says
Bill-
This is so close to what I need to do on my custom blog page. Right now, I created a bare bones page_blog.php and added that to my child theme. Right now genesis is only pulling in the ‘post’ type. I’d like to to query ‘post’ and ‘podcast’.
What should I add to my blog template to accomplish this? Thanks again for your such good resources.
-Dan
Bill Erickson says
I do not recommend you use a custom page template like page_blog.php. You should be modifying the main WordPress query as described above.
1. Delete your page_blog.php file
2. Go to Settings > Reading and set your blog page as the one that displays your posts
3. In functions.php, add this: https://gist.github.com/billerickson/9056496
Dan R. says
That would be okay, except in this theme, the front page- is a rather nice page (http://www.appendipity.com/pintercast/) that I don’t want to lose to a standard blog page.
What this theme offers is a nice dynamic front page- then a blog page which pulls in podcasts and posts. Except that I can’t get the blog page to pull in CPT: ‘podcast’
Make sense?
-Dan
Bill Erickson says
That’s fine. Use front-page.php for your front page, and home.php for your blog page.