23.4 Taxonomy Templates: List and Term Pages
Right, let’s talk about the pages WordPress generates for your taxonomies. You’ve defined these beautiful structures to organize your content, and now WordPress, like a well-meaning but slightly clumsy intern, has to figure out how to present them to the world. It does this with two types of pages: the list (the archive of all posts in a term) and the term page itself (which is often just a more specific archive). The system is powerful, but it has its… quirks. We’ll navigate them together.
The Template Hierarchy: WordPress’s Guessing Game
When someone clicks on a “Recipes” category link, WordPress doesn’t have a single “category.php” file it’s doomed to use forever. Instead, it plays a game of “hot and cold” with your theme’s files, looking for the most specific template it can find. This is the Template Hierarchy, and for taxonomies, it’s your best friend. The order of specificity goes like this:
taxonomy-{taxonomy}-{term}.php: Looking fortaxonomy-cuisine-italian.php? That’s the one. Hyper-specific.taxonomy-{taxonomy}.php: e.g.,taxonomy-cuisine.phpfor any term in your ‘cuisine’ taxonomy.taxonomy.php: The catch-all for any custom taxonomy. Your generic fallback.archive.php: If all else fails, it’ll use the general archive template.index.php: The final, “well, we tried nothing and we’re all out of ideas” fallback.
The same logic applies to good old WordPress tags (tag-{slug}.php > tag.php > archive.php) and categories (category-{slug}.php > category.php > archive.php). The key takeaway: you are in control. You don’t have to settle for the generic archive.php look for your custom “Product Brand” taxonomy.
Making Your Term Listings Useful
The default term archive loop is… fine. It’s a list of posts. But you can do better. The real power comes from using taxonomy-specific functions within these templates. Let’s say you have a taxonomy-cuisine.php template. You can make it actually look like a cuisine page.
// In your taxonomy-cuisine.php file
<?php if ( have_posts() ) : ?>
<header class="page-header">
<h1 class="page-title">Recipes: <?php single_term_title(); ?></h1>
<?php
// The term description! So useful, so often forgotten.
the_archive_description( '<div class="taxonomy-description">', '</div>' );
?>
</header>
<?php while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<?php the_excerpt(); ?>
</article>
<?php endwhile; ?>
<?php the_posts_navigation(); ?>
<?php else : ?>
// Your "no posts found" content here
<?php endif; ?>
See that the_archive_description()? That outputs the description you (hopefully) filled out when you created the term “Italian” in the admin. This is your chance to add introductory text, SEO keywords, whatever. Use it. Otherwise, you’re just leaving a powerful content field on the table.
The “No Posts Found” Conundrum
Here’s a classic pitfall: you create a beautiful taxonomy-cuisine.php template, assign it to a term, and then… you publish a post in that term. The template works perfectly. You high-five yourself. Then, six months later, you decide “French Cuisine” isn’t working out and you delete all posts from that term. Now someone visits the “French Cuisine” term page. What do they see?
If you didn’t plan for it, they’ll see your else condition—probably a paltry “No posts found” message. That’s a terrible user experience. It’s a dead URL that should probably redirect or, at the very least, show a more helpful message. You can handle this by checking if the term itself exists and has a description, even if it has no posts.
// A more robust approach at the top of your template
$queried_object = get_queried_object();
if ( $queried_object && ! have_posts() ) {
// We have a valid term object, but no posts in it.
echo '<h1>' . esc_html( $queried_object->name ) . '</h1>';
if ( ! empty( $queried_object->description ) ) {
echo '<div class="taxonomy-description">' . esc_html( $queried_object->description ) . '</div>';
}
echo '<p>Sorry, no recipes are currently listed for this cuisine. Check back later!</p>';
// Maybe even get_template_part( 'components/recipe-subscribe-cta' );
return; // Stop the rest of the template from loading, including the loop.
}
// Otherwise, proceed with your normal loop as shown above.
This way, the page remains a useful, valid page about “French Cuisine” that manages user expectations, rather than a confusing error state.
When You Need More Than the Loop: get_terms()
Sometimes, the term page itself isn’t enough. Maybe you want to show a list of all terms in your “cuisine” taxonomy in a sidebar nav. You don’t use the main WordPress Loop for that; you dip into the taxonomy system directly with get_terms(). This function is powerful but famously badly named (it returns all terms, not just those from a specific taxonomy unless you tell it to). Here’s how you use it without shooting yourself in the foot.
$args = array(
'taxonomy' => 'cuisine', // CRITICAL: Without this, it gets ALL taxonomies. Why, WordPress? Why?
'hide_empty' => false, // Set to true if you only want terms with posts assigned
'orderby' => 'count', // Order by number of posts? Alphabetically? Your call.
'order' => 'DESC'
);
$all_cuisines = get_terms( $args );
if ( ! empty( $all_cuisines ) && ! is_wp_error( $all_cuisines ) ) {
echo '<ul class="cuisine-list">';
foreach ( $all_cuisines as $cuisine ) {
$term_link = get_term_link( $cuisine );
if ( ! is_wp_error( $term_link ) ) {
printf( '<li><a href="%s">%s</a> (%d)</li>',
esc_url( $term_link ),
esc_html( $cuisine->name ),
$cuisine->count
);
}
}
echo '</ul>';
}
Always, always check if the result is a WP_Error object. Taxonomy functions love to return errors if something’s slightly off, and if you don’t check, your site will throw a warning right there in the sidebar. Not a good look. Also, note the use of get_term_link() instead of trying to manually construct the URL. It handles the rewrites for you. Let WordPress do the tedious work; that’s what it’s for.