How to use pre_get_posts

pre_get_posts happens just before a WP_Query needs to go to the database to grab stuff. Since every page has a main query, that can be changed.

Does it go in the search template?

No! By the time templates are loaded, the main query has already decided what to grab and gone to the database. Newcomers usually think the template determines the posts, but it’s actually the other way around! The query runs, then the query is used to figure out if we should load page.php or archive.php etc.

When you call functions like is_search or is_single, it’s actually asking the main query. So, it’s too late if it’s in the template.

This means that to use pre_get_posts, you have to use it in:

  • plugins
  • functions.php
  • On hooks that happen before the main query runs

Because the query is what determines which type of template is shown, you can actually mess with the query variables to change that.

Additionally, you might be wondering where it gets the values from to begin with, they come from the rewrite rules:

  1. example.com/hello-world
  2. example.com/index.php?name=hello-world
  3. new WP_Query( [ 'name' => 'hello-world' ] )
  4. pre_get_posts
  5. Use query object and see that it’s a single post
    a. Try to load single-hello-world.php
    b. Try to load single.php
    c. etc…

Also, does it work differently when I’m querying a taxonomy archive page?

Yes and no. Remember that pre_get_posts runs for all queries. Not just the main query, but widget queries, RSS, REST API, admin area, etc.

This means, if you use the filter, you can’t just set options, as they’ll apply site wide. You have to specifically check you’re on the query and page you desire.

So How Do I Use pre_get_posts?

Here’s an example from the official docs that sets the movies post type archive to 50 posts per page:

function hwl_home_pagesize( $query ) {
    if ( ! is_admin() && $query->is_main_query() && is_post_type_archive( 'movie' ) ) {
        // Display 50 posts for a custom post type called 'movie'
        $query->set( 'posts_per_page', 50 );
        return;
    }
}
add_action( 'pre_get_posts', 'hwl_home_pagesize', 1 );

But that’s not built super great, so lets use an improved version:

function hwl_home_pagesize( \WP_Query $query ) {
    if ( is_admin() ) {
        return; // we want the frontend! exit if it's WP Admin
    }
    if ( !$query->is_main_query() ) {
        return; // we want the main query!
    }
    if ( ! is_post_type_archive( 'movie' ) ) {
        return; // we only want the movie archives
    }
    // On the movie archive, show 50 posts
    $query->set( 'posts_per_page', 50 );
}

add_action( 'pre_get_posts', 'hwl_home_pagesize', 1 );

Notice that we do checks first to make sure we’re only setting the posts_per_page when we want. For a search page, you might check $query->is_search(), etc

What If My Template Uses a WP_Query?

If your template ignores the main loop, and uses its own instead, then stop doing that. Use pre_get_posts to change what the main loop shows instead.

Note, that you can modify these queries with pre_get_posts, but, is_main_query will be false, and you’ll need to add specialised code to target that specific query, otherwise everything from widgets to nav menus might be impacted