Practical Uses of pre_get_posts

In WordPress, pre_get_posts is an action that makes it possible to modify an existing WP_Query, before that query is actually run. pre_get_posts offers some solutions that are more performant than writing a custom WP_Query, and enables solutions to other problems that would be quite difficult otherwise.

Let’s learn about pre_get_posts!

What a WP_Query Is

Almost every page on a WordPress site is created by looping through bundles of posts, which are fetched using WP_Query.

In programming, to query a database generally means to fetch specific results out of that database. For example, in a database full of person data, you might want a specific person’s name and age, and you’d write a query (in SQL or a similar language) to fetch the information you want.

A WP_Query is a special type of query, unique to WordPress, that fetches posts from the WordPress database. WordPress posts are the blocks of content that make up a site’s blog articles, pages, media attachments, and more.

As we cover in our introduction to WP_Query, almost every page on a WordPress site is created by looping through one or more bundles of posts. These post bundles are fetched by the default WP_Query that WordPress runs for that page type, and may also be fetched by one or more custom WP_Querys we write ourselves.

pre_get_posts in a Nutshell

pre_get_posts is an action that lets you modify a WP_Query that is about to run.

pre_get_posts is an action that lets you modify a WP_Query that is “about to run,” meaning “about to ask the database for a post bundle.” Before the query can run off to the database and get its bundle of posts, we’re going to swoop in and change which posts the query will actually request. This order of events is how pre_get_posts gets its name.

For example, the following pre_get_posts example will cause your blog index page, which normally lists your most recent posts of type Post, to instead list your most recent posts of type Page:

add_action( 'pre_get_posts', 'wpshout_pages_blogindex' );
function wpshout_pages_blogindex( $query ) {
	if ( is_home() && $query->is_main_query() ) :
		$query->set( 'post_type', 'page' );
	endif;
}

Pasting this code into the live theme’s functions.php leads to a (rather odd) blog index with only Pages and no Posts. On the blog page of our agency site Press Up, that looks as follows:

pre_get_posts: blog index pages post type

So pre_get_posts let us modify the blog index’s already existing default query, without us having to write a new WP_Query ourselves. That’s pre_get_posts in a nutshell.

When to Use pre_get_posts

Remember, pre_get_posts lets you modify an existing query. So pre_get_posts is useful when it’s best not to write a new query, but to change one that already exists. Here are a few examples:

Some Custom WP_Querys are Easiest to Modify with pre_get_posts

Sometimes you need to modify a custom WP_Query that you didn’t write. This most likely means a query from an external plugin, or from somewhere in WordPress core.

As an example, the “Recent Posts” widget is a part of WordPress core. Let’s say that we’re using “Recent Posts” on our site, and that we’ve just created a page called “Fundraiser.” On our Fundraiser page, and that page alone, we want the “Recent Posts” widget to only show recent posts that are tagged with “fundraiser.”

We could install a better “Recent Posts” widget that allows this, and then make a custom widget area exclusively for the “Our Fundraiser” page, but that creates quite a bit of clutter. Instead, let’s just conditionally modify the custom WP_Query that “Recent Posts” uses. That process has three steps:

  1. Make sure we’re on the “Fundraiser” page,
  2. Make absolutely sure we’re talking to the custom query generated by “Recent Posts” (the hardest part), and
  3. Modify that query to our liking.

Here’s the code:

add_action( 'pre_get_posts', 'wpshout_fundraiser_recent_posts' );
function wpshout_fundraiser_recent_posts( $query ) {
	// Do nothing if not on Fundraiser page
	if( ! is_page( 'Fundraiser' ) ) :
		return;
	endif;

	// Make sure we're talking to the WP_Query
	// generated by the Recent Posts widget
	if( ! is_main_query() &&
		$query->query[ 'no_found_rows' ] === true &&
		$query->query[ 'post_type' ] === NULL
	) :

		// Fetch only posts tagged with "fundraiser"
		$taxquery = array(
			array(
				'taxonomy' => 'post_tag',
				'field' => 'slug',
				'terms' => array( 'fundraiser' ),
			)
		);
		$query->set( 'tax_query', $taxquery );

	endif;
}

A bit of a convoluted example, but still probably quicker and cleaner than the alternatives if this was a real project.

Modifying WordPress Default Queries

Most of the time you’ll use pre_get_posts on WordPress’s default queries, since you can usually modify other queries directly.

WordPress’s default queries are extremely important: they’re the queries that fetch the most recent Posts for your blog index, all Posts of a specific category for a Category archive page, all posts from a specific month for a month-based archive page, search results for a search page, and so on.

In the previous section, I stretched my brain to find a case where you’d use pre_get_posts to modify a custom WP_Query. However, most of the time you’ll be using pre_get_posts on WordPress’s default queries, since you can usually modify other queries directly. (Almost every example on the Codex documentation for pre_get_posts modifies a page’s default query.)

pre_get_posts is the best way to modify a default query. Here’s an important example from last week’s article on custom post types, causing a newly registered custom post type, Courses, to show up on the site’s archive pages, which wouldn’t happen by default:

// Make Courses posts show up in archive pages
add_action( 'pre_get_posts', 'wpshout_add_custom_post_types_to_query' );
function wpshout_add_custom_post_types_to_query( $query ) {
	if( 
		is_archive() &&
		empty( $query->query_vars['suppress_filters'] )
	) {
		$query->set( 'post_type', array( 
			'post',
			'course'
		) );
	}
}P

This code first asks if we’re on an archive page. If we are, it modifies the page’s default query to pull in not only Posts, but also Courses.

The alternative to this approach—modify whichever template uses the default query you want to change (index.php, search.php, etc.), remove the template’s default query processing, and then write your own custom WP_Query from scratch—is both brittle and way too much work. For example, I’d have no idea how to write a single custom WP_Query to cover all the different kinds of queries that get generated for archive pages.

Check out the Codex for lots of other commonsense examples that relate to hiding a specific post from search results, modifying the categories that the blog index pulls in, and so on.

pre_get_posts is More Performant than Writing a New WP_Query

Use pre_get_posts instead of new WP_Query() when possible, because this minimizes queries against the WordPress database.

In some situations, you could use either pre_get_posts or a new custom WP_Query to do what you want. In those situations, it’s faster and better to use pre_get_posts, because you only make one trip—and not two—to the WordPress database. Remember, every WP_Query is a new, complex query that will be sent to the WordPress database. One modified database query will process a lot quicker than two completely separate queries.

Now You Can pre_get_posts

Working with pre_get_posts is a very helpful tool to have in your pocket if you’re running into queries you can’t modify. It’s also an excellent way to prevent writing unnecessary custom WP_Querys when it’s both simpler and more performant to modify and existing query. This is especially true when you need to make changes to WordPress’s default queries, for pages like your blog index, archive, and search pages; that’s where pre_get_posts becomes indispensable.

Thanks for reading!

Image credit: John Nakamura Remy


4 Responses

Comments

  • surja says:

    Thank you for the article, explains the concept very clearly.

  • Theo says:

    Thank you for your post.

    Shouldn’t you be using add_action(), rather than add_filter() in your examples? Technically I believe both will work, but according to the codex, it should be add_action().

    e.g. add_action( ‘pre_get_posts’, ‘wpshout_pages_blogindex’ );

Pingbacks

The WPShout Pro Community is here! One-to-one help. Never be stuck again. Master WordPress development. Take a look
Hello. Add your message here.