Customizing WordPress Archives For Categories, Tags And Other Taxonomies
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.
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:
- “Template Hierarchy,” WordPress Codex
- “Template Hierarchy,” Chip Bennett A flow chart
- The WordPress Template Hierarchy: A Mini Resource, Rami Abraham and Michelle Schulp An interactive chart
- Reveal Template, Scott Reilly A WordPress plugin
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
- Building A Custom Archive Page For WordPress
- Website Archives Design: Good Practices and Examples
- A Detailed Guide To WordPress Custom Page Templates