定制 WordPress 的默认 Query

One of the most powerful features of WordPress is the WP 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. Some examples:

  • Don’t display posts from Category X on the homepage
  • Increase or decrease the number of posts displayed per page for a specific post type
  • Determine which posts are shown and their order based on postmeta
  • Inside your page, do a separate query for different content

Andrew Nacin recently gave a great talk on using the query, which you can look at for some more technical information on what’s happening behind the scenes. I’m going to focus this tutorial on common uses.

First, what not to do. Don’t use query_posts(). As you can see from the Codex page, there’s a lot of caveats to it. I really can’t think of an instance where this function is advisable.

There’s two approaches you should take depending on your needs.

Create a new query to run inside your page or template.

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.

For this you’ll use the WP_Query class. For more information, see my post on Custom WordPress Queries. Also take a look at my Display Posts Shortcode plugin, which might save you from having to write any code.

Customize the Main Query

If you want to alter which content is returned on a page, you most likely will be modifying the main query. The first three examples above all require altering the main query.

WordPress has a very handy hook called pre_get_posts. It fires once all the query settings are ready but right before the actual query takes place. This is where we’ll jump in and modify the query settings if needed.

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 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 everything from nav menus to recent comments widgets. We’ll do this by checking $query->is_main_query()This was added in WordPress 3.3. For earlier versions, compare$query to the global $wp_the_query variable, as shown below in the 3.2 code below.

Then we’ll check to make sure the conditions are right for our modification. If you only want it on the homepage, we’ll make sure the query is for home ( $query->is_home() ).

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 theWP_Query Codex page.

Exclude Category from Blog

<?php

add_action( 'pre_get_posts', 'be_exclude_category_from_blog' );

function be_exclude_category_from_blog( $query ) {

	if( $query->is_main_query() && $query->is_home() ) {
		$query->set( 'cat', '-4' );
	}

}

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 this category.

If we were using WordPress 3.2 or earlier, the code could look like this:

<?php

add_action( 'pre_get_posts', 'be_exclude_category_from_blog' );

function be_exclude_category_from_blog( $query ) {

	global $wp_the_query;
	if( $wp_the_query === $query && $query->is_home() ) {
		$query->set( 'cat', '-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
<?php

add_action( 'pre_get_posts', 'be_change_event_posts_per_page' );

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' );
	}

}

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 (so not ones that have ended), 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.

Creating the metabox and the fields within it are beyond the scope of this tutorial, but take a look at my post on Custom Metaboxes and this Events Plugin if you you want more informaton.

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
<?php

function be_event_query( $query ) {

	if( $query->is_main_query() && !is_admin() && 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' );