Select Page

In part one of this series, “Restore default WordPress search,” a commenter on my LouiseTreadwell.com blog asked me to this tutorial one step further by narrowing the search results and then I had a client ask me if we could eliminate specific pages. (The commentor also asked about bumping pages vs posts in priority, so we might tackle that in the future)

Your first instinct might be to attempt to edit the loop that we called in the search.php template file. But that would be wrong, silly, and cumbersome. Instead, we can insert a function to alter the search results before they even reach the search page.

Filtering search results using pre_get_posts

To start, decide exactly what you want to exclude from the search. A category of posts? One post? One page? Several specific pages or posts? And then go about trying to identify the id numbers or slugs that correspond to those pages, posts, categories, or parent pages. Let’s run through one example.

Excluding search results by category

For this example, I am going to exclude all posts that are in the “News” category. Head over to the WordPress admin dashboard, hover over the “Posts” menu item and then select “Categories.”

Screenshot of the WordPress admin dashboard menu with the "Posts" and "Categories" menu items highlighted

You are now looking at list of all of the available categories in your current WordPress installation. In my example, I have a category called “News” whose slug is also “news.” How can I figure out what it’s ID is? Just click on the the category as if you were going to edit it.

Screenshot of WordPress categories dashboard

When you are in edit mode for a particular category, you can now view lots of important information in the browser address bar. Take a peek! The taxonomy type is there along with the ID. For tags and catgegories, the ID is the tag_ID.

Identify the taxonomy type and tag ID in the browser address bar

Our taxonomy is “category” and our tag_ID is “3.” We can use this information to build out our function. We will be creating a callback function named “isg_search_filter” and use the “add_action” action hook to bind our new callback to WordPress’ “pre_get_posts” action. The “pre_get_posts” action lets us modify the content of WordPress’s default loop before it displays results on our search page.

You can name your callback function whatever you wish. Just make sure the name is unique to your WordPress installation. I like using “isg” at the start of mine so you might find it helpful to pick out your own initials for yours. The action hook “add_action” can’t be changed and the action hook “pre_get_posts” must remain the same as well. as those are WordPress specific.

Did you know?add_action” is almost an alias of “add_filter“. They behave in very similar ways and can be used interchangeably in some instances.

https://developer.wordpress.org/plugins/hooks/#actions-vs-filters
function isg_custom_search( $query ) {
    if ( $query->is_search && !is_admin() )
        $query->set( 'cat','-3' );
    return $query;
}
add_action( 'pre_get_posts', 'isg_custom_search' );

Placing this snippet in your functions.php file will keep any posts in the “News” category from displaying in the results. What if we wanted to target content a different way?

Excluding the child pages of one parent from search results

Let’s dig into that snippet and decide how to modify it. The only piece that we really need to think about is this particular line:

$query->set( 'cat','-3' );

Instead of targeting a specific category, we want to identify a specific parent page. For one of my current clients, they want their “Thank You” pages for their newsletter sign-ups to be excluded from the default search. All of those pages conveniently are child of a page that has the slug “/thankyou/” and a page ID of “1166.” How do we know that? By going to edit that page and then identifying the number listed in the browser address bar the same way we did in the above example with the category.

A screenshot of a WordPress page in edit mode. Emphasis is on the part of the browser address that shows post=1166.

How do we write a query that will specifically target the children of page that has the slug “/thankyou/” or the ID of “1166”? We know that the function get_pages() will return an array of pages based on what arguments we feed it. In this case, the argument we want to use is “child_of” along with the ID of the parent post. We’ll tell WordPress to grab all of the children of that page and store the array in a variable called $thankyoupages.

So the first part of our function will look something like this. You could drop this into a page template just to see what it outputs.

//Build an array of pages that are a child of page 1166
$thankyoupages = get_pages( array( 'child_of' => 1166) );
//Check to make sure the array isn't empty
if ( $thankyoupages) :
//Print out a test display of all the items in the array
print_r($thankyoupages);
endif; 

Did you try it? What did you get? An absolutely ridiculous massive amount of content that is far beyond what we actually need? Yeah. NOT very helpful.

Knowing how to grab the smallest amount of content possible is an important part of writing clean code. In the end, what do we REALLY need? We just need a query that excludes the children of a particular parent page. Nothing more.

So, let’s go to the handy dandy WordPress Codex and see what options may be available for us in the WP_Query class. There are a LOT. So many. So let’s do a “Find on this page” search for the word “parent” and see where that gets us.

Here are a few great candidates:

  • post_parent (int) – use page id to return only child pages. Set to 0 to return only top-level entries.
  • post_parent__in (array) – use post ids. Specify posts whose parent is in an array. (available since version 3.6)
  • post_parent__not_in (array) – use post ids. Specify posts whose parent is not in an array. (available since version 3.6)

The first one looks like a great option if I want to restrict my search output to only top level pages. But I don’t mind most child pages showing. I only want to restrict some specific ones. So let’s test out the second two options.

//This doesn't work!
$query->set('post_parent__in', array(-1166));
//This does work! 
$query->set('post_parent__not_in', array(1166));

Why does one work and not the other? That is a topic for another post! But now that we know that “post_parent__not_in” is the argument that we want, let’s plug it into our function.

function isg_custom_search( $query ) {
    if ( $query->is_search && !is_admin() )
        $query->set( 'cat','-97' );
        $query->set('post_parent__not_in', array(1166));
    return $query;
}
add_action( 'pre_get_posts', 'isg_custom_search' );

Beautiful! Except we are still getting the parent /thankyou/ page in our results. So let’s add in one more line to exclude that. We already know it’s ID and we can use that WP_Query page to figure out the correct argument to use.

function isg_custom_search( $query ) {
    if ( $query->is_search && !is_admin() )
        $query->set( 'cat','-97' );
        $query->set('post_parent__not_in', array(1166));
        $query->set( 'post__not_in', array( 1166 ) );
    return $query;
}
add_action( 'pre_get_posts', 'isg_custom_search' );

Voila! Our search results are now excluding any posts from the “news” category, the “/thankyou/” page, and its children.

Conclusion

The action hook “pre_get_posts” can accomplish a lot of things. Give this snippet a spin in your own WordPress sandbox and share your snippet variations in the comments below!

*Let me know if you see any typos in this post so I can correct them!