Customizing WordPress Archives For Categories, Tags And Other Taxonomies

About The Author

Josh Pollock writes about WordPress, does theme and plugin development, serves as the community manager for the Pods Framework and advocates open source … More about Josh ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

If you use custom post types in WordPress, you might need to organize them like categories and tags. Categories and tags are examples of taxonomies, and WordPress allows you to create as many custom taxonomies as you want. In this article, Josh Pollock will explain custom taxonomies and how to create them. He’ll also go over which template files in a WordPress theme control the archives of built-in and custom taxonomies, and some advanced techniques for customizing the behavior of taxonomy archives.

Most WordPress users are familiar with tags and categories and with how to use them to organize their blog posts. If you use custom post types in WordPress, you might need to organize them like categories and tags. Categories and tags are examples of taxonomies, and WordPress allows you to create as many custom taxonomies as you want. These custom taxonomies operate like categories or tags, but are separate.

In this tutorial, we’ll explain custom taxonomies and how to create them. We’ll also go over which template files in a WordPress theme control the archives of built-in and custom taxonomies, and some advanced techniques for customizing the behavior of taxonomy archives.

Terminology

Before continuing, let’s get our terminology straight. A taxonomy is a WordPress content type, used primarily to organize content of any other content type. The two taxonomies everyone is familiar with are built in: categories and tags. We tend to call an individual posting of a tag a “tag,” but to be precise, we should refer to it as a “term” in the “tag” taxonomy. We pretty much always refer to items in a custom taxonomy as “terms.”

Categories and tags represent the two types of taxonomies: hierarchical and non-hierarchical. Like categories, hierarchical taxonomies can have parent-child relationships between terms in the taxonomy. For example, you might have on your blog a “films” category that has several child categories, with names like “foreign” and “domestic.” Custom taxonomies may also be hierarchical, like categories, or non-hierarchical, like tags.

wordpress-categories-taxonomies-opt
A small part of the "WordPress Template Hierarchy". (Source)

The archive of a taxonomy is the list of posts in a taxonomy that is automatically generated by WordPress. For example, this would be the page you see when you click on a category link and see all posts in that category. We’ll go over how to change the behavior of these pages and learn which template files generate them.

How Tag, Category And Custom Taxonomy Archives Work

For every category, tag and custom taxonomy, WordPress automatically generates an archive that lists each post associated with that taxonomy, in reverse chronological order. The system works really well if you organize your blog posts with categories and tags. If you have a complex system of organizing custom post types with custom taxonomies, then it might not be ideal. We’ll go over the many ways to modify these archives.

The first step to customizing is to know which files in your theme are used to display the archive. Different themes have different template files, but all themes have an index.php template. The index.php template is used to display all content, unless a template exists higher up in the hierarchy. WordPress’ template hierarchy is the system that dictates which template file is used to display which content. We’ll briefly go over the template hierarchy for categories, tags and custom taxonomies. If you’d like to learn more, these resources are highly recommended:

Most themes have an archive.php template, which is used for category and tag archives, as well as date and author archives. You can add a template file to handle category and tag archives separately. These templates would be named category.php or tag.php, respectively. You could also create templates for specific tags or categories, using the ID or slug of the category or tag. For example, a tag with the ID of 7 would use tag-7.php, if it exists, rather than tag.php or archive.php. A tag with the slug of “avocado” would be displayed using the tag-avocado.php template.

One tricky thing to keep in mind is that a template named after a slug will override a template named after an ID number. So, if a tag with the slug of “avocado” had an ID of 7, then tag-avocado.php would override tag-7.php, if it exists.

The template hierarchy for custom taxonomies is a little different, because there are templates for all taxonomies, for specific taxonomies and for specific terms in a specific taxonomy. So, imagine that you have two taxonomies, “fruits” and “vegetables,” and the “fruits” taxonomy has two terms, “apples” and “oranges,” while “vegetables” has two terms, “carrots” and “celery.” Let’s add three templates to our website’s theme: taxonomy.php, taxonomy-fruits.php and taxonomy-vegetables-carrots.php.

For the terms in the “fruits” taxonomy, all archives would be generated using taxonomy-fruits.php because no term-specific template exists. On the other hand, the term “carrots” in the “vegetables” taxonomy’s archives would be generated using taxonomy-vegetables-carrots.php. Because no taxonomy-vegetables.php template exists, all other terms in “vegetables” would be generated using taxonomy.php.

Using Conditional Tags

While you can add any of the custom templates listed above to create a totally unique view for any category, tag, custom taxonomy or custom taxonomy term, sometimes all you want to do is make one or two little changes. In fact, try to avoid creating a lot of templates because you will need to adjust each one when you make overall changes to the basic HTML markup that you use in each template in the theme. Unless I need a template that is radically different from the theme’s archive.php, I tend to stick to adding conditional changes to archive.php.

WordPress provides conditional functions to determine whether a category, tag or custom taxonomy is being displayed. To determine whether a category archive is being shown, you can use is_category() for categories, is_tag() for tags and is_tax() for custom taxonomies. The is_tag() and is_category() functions can also test for specific categories or tags by slug or ID. For example:


<?php
    if ( is_tag() ) {
        echo "True for any tag!";
    }
    if ( is_tag( 'jedis' ) ) {
        echo "True for the tag whose slug is jedi";
    }
    if ( is_tag( array( 'jedi', 'sith' ) ) ) {
        echo "True for tags whose slug is jedi or sith";
    }
    if ( is_tag( 7 ) ) {
        echo "You can also use tag IDs. This is true for tag ID 7";
    }
?>

For custom taxonomies, the is_tax() function can be used to check whether any taxonomy (not including categories and tags), a specific taxonomy or a specific term in a taxonomy is being shown. For example:


<?php
    if ( is_tax() ) {
        echo "True for any custom taxonomy.";
    }
    if ( is_tax( 'vegetable' ) ) {
        echo "True for any term in the vegetable taxonomy.";
    }
    if ( is_tax( 'vegetable', 'celery' ) ) {
        echo "True only for the term celery, in the vegetable taxonomy.";
    }
?>

Creating Custom Taxonomies

Adding a custom taxonomy can be done in one of three ways: coding it manually according to the instructions in the Codex, which I don’t recommend; generating the code using GenerateWP; or using a plugin for custom content types, such as Pods or Types. Plugins for custom content types enable you to create custom taxonomies and custom post types in WordPress’ back end without having to write any code. Using one is the easiest way to add a custom taxonomy and to get a framework for working with custom content types.

If you opt for one of the first two options, rather than a plugin, then you will need to add the code either to your theme’s functions.php file or to a custom plugin. I strongly recommend creating a custom plugin, rather than adding the code to functions.php. Even if you’ve never created a plugin before, I urge you to do it. While adding the code to your theme’s functions.php will work, when you switch themes (say, because you want to use a new theme or to troubleshoot a problem), the taxonomy will no longer work.

Whether you write your custom taxonomy code by following the directions in the Codex or by generating it with GenerateWP, just paste it in a text file and add one line of code before it and you’ll have a plugin. Upload it and install it as you would any other plugin.

The only line you need to create a custom plugin is /* Plugin name: Custom Taxonomy */.

Below is a plugin to register a custom taxonomy named “vegetables,” which I created using GenerateWP because it’s significantly easier and way less likely to contain errors than doing it manually:


<?php
    /* Plugin Name: Veggie Taxonomy */
    if ( ! function_exists( 'slug_veggies_tax' ) ) {

    // Register Custom Taxonomy
    function slug_veggies_tax() {

    $labels = array(
    'name'                          => _x( 'Vegetables', 'Taxonomy General Name', 'text_domain' ),
    'singular_name'                 => _x( 'Vegetable', 'Taxonomy Singular Name', 'text_domain' ),
    'menu_name'                     => __( 'Taxonomy', 'text_domain' ),
    'all_Veggies'                   => __( 'All Veggies', 'text_domain' ),
    'parent_Veggie'                 => __( 'Parent Veggie', 'text_domain' ),
    'parent_Veggie_colon'           => __( 'Parent Veggie:', 'text_domain' ),
    'new_Veggie_name'               => __( 'New Veggie name', 'text_domain' ),
    'add_new_Veggie'                => __( 'Add new Veggie', 'text_domain' ),
    'edit_Veggie'                   => __( 'Edit Veggie', 'text_domain' ),
    'update_Veggie'                 => __( 'Update Veggie', 'text_domain' ),
    'separate_Veggies_with_commas'  => __( 'Separate Veggies with commas', 'text_domain' ),
    'search_Veggies'                => __( 'Search Veggies', 'text_domain' ),
    'add_or_remove_Veggies'         => __( 'Add or remove Veggies', 'text_domain' ),
    'choose_from_most_used'         => __( 'Choose from the most used Veggies', 'text_domain' ),
    'not_found'                     => __( 'Not Found', 'text_domain' ),
    );
    $args = array(
    'labels'                     => $labels,
    'hierarchical'               => false,
    'public'                     => true,
    'show_ui'                    => true,
    'show_admin_column'          => true,
    'show_in_nav_menus'          => true,
    'show_tagcloud'              => false,
    );
    register_taxonomy( 'vegetable', array( 'post' ), $args );

    }

    // Hook into the 'init' action
    add_action( 'init', 'slug_veggies_tax', 0 );

    }
?>

By the way, I created this code using GenerateWP in less than two minutes! The service is great, and manually writing code that this website can automatically generate for you makes no sense. To make the process even easier, you can use the plugin Pluginception to create a blank plugin for you and then paste the code from GenerateWP into it using WordPress’ plugin editor.

Using WP_Query With Custom Taxonomies

Once you have added a custom taxonomy, you might want to query for posts with terms in that taxonomy. To do this, we can use taxonomy queries with WP_QUERY.

Taxonomy queries can be very simple or complicated. The simplest query would be for all posts with a certain term. For example, if you had a post type named “jedi” and an associated custom taxonomy named “level,” then you could get all Jedi masters like this:


<?php
    $args = array(
        'post_type' => 'jedi',
        'level' => 'master'
    );
    $query = new WP_Query( $args );
?>

If you added a second custom taxonomy named “era,” then you could find all Jedi masters of the Old Republic like this:


<?php
    $args = array(
        'post_type' => 'jedi',
        'level' => 'master',
        'era' => 'old-republic',
    );
    $query = new WP_Query( $args );
?>

We can also do more complicated comparisons, using a full tax_query. The tax_query argument enables us to search by ID instead of slug (as we did before) and to search for more than one term. It also enables us to combine multiple taxonomy queries and to set the relationship between the two. In addition, we can even use SQL operators such as NOT IN to exclude terms.

The possibilities are endless. Explore the “Taxonomy Parameters” section of the Codex page for “Class Reference/WP_Query” for complete information. The snippet below searches our “jedi” post type for Jedi knights and masters who are not from the Old Republic era:


<?php
    $args = array(
        'post_type' => 'jedi',
        'tax_query' => array(
        'relation' => 'AND',
            array(
                'taxonomy' => 'level',
                'field' => 'slug',
                'terms' => array( 'master', 'knight' )
            ),
            array(
                'taxonomy' => 'era',
                'field' => 'slug',
                'terms' => array( 'old-republic' ),
                'operator' => 'NOT IN'
                )
        )
    );
    $query = new WP_Query( $args );
?>

Customizing Taxonomy Archives

So far, we have covered how taxonomies, tags and categories work by default, as well as how to create custom taxonomies. If any of this default behavior doesn’t fit your needs, you can always modify it. We’ll go over some ways to modify WordPress’ built-in functionality for those of you who use WordPress less as a blogging platform and more as a content management system, which often requires custom taxonomies.

Hello pre_get_posts

Before any posts are outputted by the WordPress loop, WordPress automatically retrieves the posts for the user according to the page they are on, using the WP_QUERY class. For example, in the main blog index, it gets the most recent posts. In a taxonomy archive, it gets the most recent posts in that taxonomy.

To change that query, you can use the pre_get_posts filter before WordPress gets any posts. This filter exposes the query object after it is set but before it is used to actually get any posts. This means that you can modify the query using the class methods before the main WordPress loop is run. If that sounds confusing, don’t worry — the next few sections of this article give practical examples of how this works.

Adding Custom Post Types To Category Or Tag Archives

A great use of modifying the WP_QUERY object using pre_get_posts is to add posts from a custom post type to the category archive. By default, custom post types are not included in this query. If we were constructing arguments to be passed to WP_Query and wanted to include both regular posts and posts in the custom post type “jedi,” then our argument would look like this:


<?php
    $args = array( 'post_type' =>
        array(
            'post',
            'jedi'
        )
    );
?>

In the callback for our pre_get_posts filter, we need to pass a similar argument. The problem is that the WP_QUERY object already exists, so we can’t pass an argument to it like we do when creating an instance of the class. Instead, we use the set() class method, which allows us to change any of the arguments after the class has been created.

In the snippet below, we use set() to change the post_type argument from the default value, which is post, to an array of post types, including posts and our custom post type “jedi.” Note that we are using the conditional tag is_category() so that the change happens only when category archives are being displayed.


<?php
    add_filter( 'pre_get_posts', 'slug_cpt_category_archives' );
    function slug_cpt_category_archives( $query ) {
    if ( $query->is_category() && $query->is_main_query()  )  {
        $query->set( 'post_type',
            array(
                'post',
                'jedi'
            )
        );
    }

    return $query;

    }
?>

This function’s $query parameter is the WP_QUERY object before it is used to populate the main loop. Because a page may include multiple loops, such as those used by widgets, we use the conditional function is_main_query() to ensure that this affects only the main loop and not any secondary loops on the page, such as those used by widgets.

Making Category Or Hierarchical Taxonomy Archives Hierarchical

By default, the archives for categories and other hierarchical taxonomies act like any other taxonomy archive: they show all posts in that category or with that taxonomy term. To show only parent terms and exclude child terms, you would use the pre_get_posts filter again.

Just like when creating your own WP_QUERY for posts in a taxonomy, the main loop’s WP_QUERY uses the tax_query arguments to get posts by taxonomy. The tax_query has an include_children argument, which by default is set to 1 or true. By changing it to 0 or false, we can prevent posts with a child term from being included in the archive:


<?php
    add_action( 'pre_get_posts', 'slug_cpt_category_archives' );
    function slug_cpt_category_archives( $query ) {
        if ( is_tax( 'TAXONOMY NAME') )  {
            $tax_query = $query->tax_query->queries;
            $tax_query['include_children'] = 0;
            $query->set( 'tax_query', $tax_query );
        }

    }
?>

The result sounds desirable but has several major shortcomings. That’s OK, because if we address those flaws, we’ll have taken the first step to creating something very cool.

The first and biggest problem is that the result is not an archive page that shows the child terms; it’s still a post with the parent term. The other problem is that we don’t have a good way to navigate to the child term archives.

A good way to deal with this is to combine the pre_get_post filter above with a modification to the template that shows the category or taxonomy. We discussed earlier how to determine which template is used to output category or custom taxonomy archives. Also, keep in mind that you can always wrap your changes in conditional tags, such as is_category() or is_tax(), but that can become unwieldy quickly; so, making a copy of your archive.php and removing any unneeded code probably makes more sense.

The first step is to wrap the entire thing in a check to see whether the current taxonomy term has children. If it does not, then we do not want to output anything. To do this, we use get_term_children(), which will return an empty array if the current term has no children and which we can test for with !empty().

To make this work for any taxonomy that might be displayed, we need to get the current taxonomy and taxonomy term from the query_vars array of the global $wp_query object. The taxonomy’s slug is contained in the taxonomy key, and the term’s slug is in the tax key.

To use get_term_children(), we must have the term’s ID. The ID is not in query_vars, but we can pass the slug to get_term_by() to get it.

Here is how we get all of the information that we need into variables:


<?php
    global $wp_query;
    $taxonomy = $wp_query->query_vars['taxonomy'];
    $term = $wp_query->query_vars['tax'];
    $term_id = get_term_by( 'slug', $term, $taxonomy );
    $term_id = $term_id->term_id;
    $terms = get_term_children( $term_id, $taxonomy );
?>

Now we will continue only if $terms isn’t an empty array. To see whether it is empty in our check, first we will repopulate the terms using get_terms(). This is necessary because get_term_children returns only an array of IDs, and we need IDs and names, both of which are in the object returned by get_terms(). We can loop through this object, outputting the name as a link. The link can be generated by passing the term’s ID to get_term_link().

Here is the complete code:


<?php
    global $wp_query;
    $taxonomy = $wp_query->query_vars['taxonomy'];
    $term = $wp_query->query_vars['tax'];
    $term_id = get_term_by( 'slug', $term, $taxonomy );
    $term_id = $term_id->term_id;
    $terms = get_term_children( $term_id, $taxonomy );
    if ( !empty( $terms ) ) {
    $terms = get_terms( $taxonomy, array( 'child_of' => $term_id ) );
    echo '<ul class="child-term-list">';
    foreach ( $terms as $term ) {
    echo '<li><a href="'.$term->term_id.'">'.$term->name.'</a></li>';
    }

    echo '</ul>';

?>

Creating A Custom Landing Page For Taxonomy Archives

If your hierarchical taxonomy has no terms in the parent term, then the regular taxonomy archive system will be of no use to you. You really want to show taxonomy links instead.

In this case, a good option is to create a custom landing page for the term. We’ll use query_vars again to determine whether the user is on the first page of a taxonomy archive; if so, we will use the taxonomy_archive filter to include a separate template, like this:


<?php
    add_filter( 'taxonomy_archive ', 'slug_tax_page_one' );
    function slug_tax_page_one( $template ) {
        if ( is_tax( 'TAXONOMY_NAME' ) ) {
             global $wp_query;
             $page = $wp_query->query_vars['paged'];
            if ( $page = 0 ) {
                $template = get_stylesheet_directory(). '/taxonomy-page-one.php';
            }
        }

        return $template;

    }
?>

This callback first checks that the user is in the taxonomy that we want to target. We can target all taxonomies by changing this to just is_tax(). Then, it gets the current page using the query_var named paged, and if the user is on the first page, then it returns the address for the new template file. If not, it returns the default template file.

What you put in that template file is up to you. You can create a list of terms using the code shown above. You can use it to output any content, really — for example, more information about the taxonomy term or links to specific posts.

Taking Control

With a bit of work, WordPress’ basic architecture, which still reflects its origins as a blogging platform, can be customized to fit almost any website or Web app. Using custom taxonomies to organize your content and doing it in a way that suits your needs will be an important step in many of your WordPress projects. Hopefully, this post has brought you a step closer to getting the most out of this powerful aspect of WordPress.

Further Reading

Smashing Editorial (dp, al, il, mrn)