---
title: Search by Content Type in Drupal 8 - a custom module tutorial
description: Learn how to create a custom Drupal 8 module to exclude specific content types from search results using configuration manager, routing, and menu links.
---

[Blog ](https://www.metaltoad.com/blog)

# [Search by Content Type in Drupal 8 - a custom module tutorial](https://www.metaltoad.com/blog/search-content-type-drupal-8-custom-module-tutorial)

 Written by [Metal Toad Staff](https://www.metaltoad.com/blog/author/metal-toad-staff) | Apr 27, 2016 12:00:00 AM

**Note**: this code has been tested in Drupal 8.0 and 8.1

At the time of this writing, the [Search API module](https://www.drupal.org/project/search_api) is still in alpha phase. A note from the module maintainers warn that it is unstable. This blog post serves as a way to learn how to write a simple Drupal 8 module to modify Drupal 8's core search without using the Search API module.

**Scenario**: we have a Drupal 8 site that shows all content types in the search results. We want to exclude certain content types (also known as "node bundles") from the results list. 

**Fun D8 module stuff we'll learn along the way**:

- How to use simple, custom configurations in our module
- What TAG's are, and how they're awesome

Here's a link to the GitHub repository for this example:  
[https://github.com/mikedotexe/exclude_bundles](https://github.com/mikedotexe/exclude_bundles)

So first: the big picture. I'd like to exclude certain bundles, but I'd also like the ability to change them. (That is to say, I don't want to hardcode them.) So we're going to build a settings page for our Drupal 8 module that looks like this:

If you look at the breadcrumbs at the top, you'll see this is added under the Admin >> Configuration >> Search and metadata. So we're also going to explore doing some routing, and making a menu link in an existing menu. That will give us the ability to place our settings page in the most logical place: right next to all the other Search Stuff.

**/admin/config**

The checkboxes on our settings page will also be exportable using Drupal 8's Configuration Manager.  
(drush cex is the configuration export command)

 

So, without further adieu, let's start looking at the code.

### exclude_bundles.info.yml

Like Drupal 7's .info file, every custom Drupal 8 module needs an info YAML file.

```
name: Exclude bundles
type: module
description: Allows Drupal core search to exclude node bundles
core: 8.x
package: Custom
configure: exclude_bundles.admin.config
```

The only line that's any fun here is the **configure** key. That gives us the neat little gear icon on **/admin/modules**

But what is the deal with **exclude_bundles.admin.config**? Shouldn't it be **/admin/config/search/bundles** or some other path? Not in Drupal 8, and this is where routing comes into play.

### exclude_bundles.routing.yml

```
exclude_bundles.admin.config:
  path: '/admin/config/search/bundles'
  defaults:
    _form: '\Drupal\exclude_bundles\Form\SearchBundles'
    _title: 'Bundles to exclude from search'
  requirements:
    _permission: 'administer search'
```

In Drupal 7, this used to be accomplished through [hook_menu](https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_menu/7.x). Here's [a good resource](https://www.drupal.org/node/2118147) on the differences betweeen Drupal 7 and Drupal 8's routing.

In the above code, the line I'd like to highlight is the **_form** key under **defaults**. As you can see, this is pointing to a class defined by our module, **exclude_bundles**, which brings us to the next file we'll look at. (Note that we omit the ".php" when referencing this class.)

### src/Form/SearchBundles.php

```
...
public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('exclude_bundles.settings');
    $configured_bundles = $config->get('bundles');
    // get list of current Content Types
    $node_bundles = \Drupal::service('entity.manager')->getStorage('node_type')->loadMultiple();
...
public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('exclude_bundles.settings')
      ->set('bundles', $form_state->getValue('bundles'))
      ->save();
...
```

I've omitted sections of that file for readability purposes, but feel free to reference [the full code here](https://github.com/mikedotexe/exclude_bundles/blob/master/src/Form/SearchBundles.php). 

The code above shows us putting Drupal 8's configuration manager into action. We're basically saying,

> "Hey Drupal, I want to use configuration. Which one? Um.. let's call it **exclude_bundles.settings**. Let's read from that configuration and save to it as well."
> 
> ~ fledgling Drupal 8 dev

Let's stop for a second and talk about Configuration Manager in Drupal 8. This is an amazing leap for Drupal, and takes the place of the very popular [Features](https://www.drupal.org/project/features) module that allows us to export configuration (Views, taxonomy vocabs, Panel variants, content types...) into code.

Say you have a team using source control, and you're tasked with making a new content type. You create the content type and fields (on your local machine) and export those changes. New config files will be created in your designated configuration directory, and you can push those up to your repository. A second developer can then pull down those changes, use configuration import, and their site will have the new content type. This blog isn't about Configuration Manager, so I'll move on from that topic, but want to set that foundation.

Why are we using configuration? Because our settings page contains checkboxes, allowing users to customize which content types should be excluded from search. These settings are placed into a **configuration** instead of a plain variable, meaning they can be exported/imported as code. It also means we can set defaults in code. If we wanted to ship our module with the default of "exclude Articles from search, but show Basic Page content in search results" you would add this file:

### config/install/exclude_bundles.settings.yml

```
base_route_name: exclude_bundles.admin.config
bundles:
  article: article
  page: 0
```

Time for the .module file, which is the most fun.

### exclude_bundles.module

```
use Drupal\Core\Database\Query\AlterableInterface;
 
/**
 * Implements hook_query_TAG_alter
 */
function exclude_bundles_query_search_node_search_alter(AlterableInterface $query) {
  // see the config file exclude_bundles.settings.yml
  $config = \Drupal::config('exclude_bundles.settings');
  $bundles_config = $config->get('bundles');
  $hidden_bundles = [];
  foreach ($bundles_config as $k => $v) {
    // if the value is not 0, add it to the array
    if ($v) {
      $hidden_bundles[] = $k;
    }
  }
 
  if (!empty($hidden_bundles)) {
    // add the extra table to join based on config logic
    $query->join('node', 'n2', 'n.nid = n2.nid');
    $query->condition('n2.type', $hidden_bundles, 'NOT IN');
  }
}
```

I read a great [blog post by Phase 2](https://www.phase2technology.com/blog/restricting-search-results-in-drupal-7/) regarding Drupal core search, and went a step further. The code above implements [hook_query_TAG_alter](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Database%21database.api.php/function/hook_query_TAG_alter/8.0.x), which I had never used before. In fact the word **TAG **had me interested.

**TAG **is very similar to **FORM_ID **in [hook_form_FORM_ID_alter](https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_form_FORM_ID_alter/7.x). 

Sure, you can use the generic [hook_form_alter ](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Form%21form.api.php/function/hook_form_alter/8.0.x)to access *any *form, but why not drill down into the exact form we're intending to modify? The same idea applies to [hook_query_alter](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Database%21database.api.php/function/hook_query_alter/8.0.x).

Where is this tag being set? I'm glad you asked, 'cause I wondered the same thing.

Let's look into Drupal core:  
**core/modules/search/src/SearchQuery.php**

 

Hence, our **TAG** is **search_node_search**, as shown in the module code above.

If we debug the tables that are included in the $query object, we'll see that there is an alias "n" for the table **node_field_data**.

In the last if statement, we are simply joining another table (the **node** table) so that we can filter by content type. Donezo.

Lastly, let's place a menu link for this. 

### exclude_bundles.links.menu.yml

```
exclude_bundles.admin.config:
  title: 'Exclude bundles'
  parent: system.admin_config_search
  description: 'Exclude content types from search.'
  route_name: exclude_bundles.admin.config
  menu_name: admin
```

To place this under the "Search and metadata" menu item, once again we'll use the route instead of the path on the line pointing to the parent. So **system.admin_config_search** is simply a route defined in the Drupal core's system module. All starting to make sense, right? Right!

And that's it! We've created a custom Drupal 8.0/8.1 module that alters Drupal core's search to filter by content type, and it uses Drupal 8's configuration manager, routing, and a menu link. I hope this demystifies some of the new module development processes for Drupal 8.

*This article is one of Metal Toad's **[Top 20 Drupal Tips](https://www.metaltoad.com/help/top-20-drupal-tips)**. Enjoy!*

[View full post](https://www.metaltoad.com/blog/search-content-type-drupal-8-custom-module-tutorial)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Metal Toad Staff"
  },
  "dateModified" : "2025-04-23T16:43:46.259Z",
  "datePublished" : "2016-04-27T00:00:00Z",
  "headline" : "Search by Content Type in Drupal 8 - a custom module tutorial",
  "image" : {
    "@type" : "ImageObject",
    "height" : 174,
    "url" : "https://6104926.fs1.hubspotusercontent-na1.net/hubfs/6104926/Imported_Blog_Media/Screenshot%202016-04-27%2016_42_44.png",
    "width" : 1013
  },
  "mainEntityOfPage" : "https://www.metaltoad.com/blog/search-content-type-drupal-8-custom-module-tutorial",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60.0,
      "url" : "https://6104926.fs1.hubspotusercontent-na1.net/hubfs/6104926/social-suggested-images/6104926.fs1.hubspotusercontent-na1.nethub6104926hubfsMetal_Toad_Logo_orange_horizontal-50-1.jpg",
      "width" : 240.00002
    },
    "name" : "Blog"
  }
}
```