Software Engineering in WordPress and Musings on the Deep Life

The Dangers of Using pre_get_posts in WordPress

In the previous post, I talked a bit about using `pre_get_posts` and how to efficiently exclude categories from the main loop.

Of course, in posts like that, the code is meant to be a point of reference or a starting place for which more advanced functionality can be written.

But one thing that I failed to mention about using `pre_get_posts` in WordPress is that it’s not limited to the public facing side of the site.

Using pre_get_posts in WordPress

As mentioned in the previous post, whenever you hook a function into the `pre_get_posts` action, then you’re passed – by reference – the `$wp_query` object. As such, the code looks something like this:

function demo_exclude_category( $wp_query ) {

    // Add the category to an array of excluded categories. In this case, though,
    // it's really just one.
    $excluded = array( '-1' );

    // Note that this is a cleaner way to write: $wp_query->set('category__not_in', $excluded);
    set_query_var( 'category__not_in', $excluded );

add_action( 'pre_get_posts', 'demo_exclude_category' );

The thing about this particular function is that it will exclude posts from that category in every single place `$wp_query` is used.

This means that the results will be excluded from places such as:

  • The RSS Feed
  • Archive Pages
  • Search Results
  • The Dashboard
  • …and so on

To that end, you’re likely only going to want to exclude it from certain places. So say, for example, that you wanted to exclude categories except from the search pages, the archives pages, and the admin dashboard.

Then the code would look something like this:

function demo_exclude_category( $wp_query ) {

    if ( ! is_admin() && ! is_search() && ! is_archive() ) {

        // Add the category to an array of excluded categories. In this case, though, it's really just one.
        $excluded = array( '-1' );

        // Note that this is a cleaner way to write: $wp_query->set('category__not_in', $excluded);
        set_query_var( 'category__not_in', $excluded );

    } // end if

add_action( 'pre_get_posts', 'demo_exclude_category' );

And for those of you who are more particular about conditionals, you could also write it to be `if ( ! ( is_admin() || is_search() || is_archive() ) )`, but that’s neither here nor there, really, for this post.

I just figure it’d come up in the comments at some point :).

Anyway, if you’re interested in the conditionals that WordPress offers, be sure to check out the Codex article on Conditional Tags as they come in handy when working with things like manipulating the main query.


  1. Norcross

    why not just include the is_main_query into your setup?

    also, I tend to just include the check for admin first, like so:

    if ( is_admin() )

    and since I often use this on both the admin side and front end side, I’ll write a separate function for each area.

    • Tom McFarlin

      This was also mentioned in the comments yesterday, which I’m totally down with (what, you mean you don’t read every comment here?! ;)

      But seriously, that’s one of the conditional tags that works – and its great – but half the point of this particular post is also to expose some of the nuanced control you can have over your work given WordPress’ conditional tags.

  2. Jason Hoffmann

    Also, I ran into some trouble with is_main_query in the admin panel like you mentioned. I was unable to sort through drafts/published/private posts before I figured out that I was blocking myself with pre_get_posts from the Codex ( Rookie mistake, I know, but I can’t be the only one. I like the idea of a separate function for is_admin instead of simply filtering out any admin queries with to ! is_admin though.

  3. Scott Lee

    Yikes! I didn’t know about this. There’s been a lot more talk and finger-pointing about using pre_get_posts and sadly this has been left out. Thanks for the heads up.

  4. Ryan Meier


    I had no idea set_query_var() was a wrapper for $wp_query->set(). This will clean things up a tad.


    • Stephen Harris

      Kind of… `set_query_var()` actually sets the query variable for the global `wp_query`. Whereas $query->set() sets if for the current WP_Query instance being passed (it might be a query for the navigation menu, related posts, posts in the sidebar etc). (Its slightly confusing as Tom is using the name `$wp_query` for the query being passed which most of the time won’t be the main query – `global $wp_query`).

      I prefer to use the conditional `$query->is_main_query()` and then use`$query->set()` to set query variable as what you are checking/doing is more obvious.

      Just a side note, in Tom’s example he’s modifying the ‘main’ query for every query. I.e. every time *any* query (on a non-admin, non-search/archive page) is run, `demo_exclude_category()` is excluding a category from the main query.

      • Ryan Meier

        I understand that. I was assuming is_main_query. Like the source shows: global.

        Thanks for the explanation.

      • Caspar Hübinger

        In addition to what Stephen said: it looks like set_query_var() somehow slips by the default Recent Posts widget, whereas $query->set() applies correctly.

        • Tom

          Indeed – thanks for adding this, Caspar!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2023 Tom McFarlin

Theme by Anders NorenUp ↑