Let's face it, taxonomy-based URLs look much nicer without the actual name of the taxonomy (/recipes/pizzas versus /recipes/recipe_categories/pizzas - assuming you have a recipe custom post type and recipe_category custom taxonomy), add nothing to the value, make your URLs longer and more complex and Google doesn't like it either (and you want to please Google, right?). So in this tutorial, I'm going to show you how to get rid of them and make your URLs nice and readable.

Use cases

So what's the scenario this could be useful in? In our example, we have a developers page that lists all our developers along with their respective tags which they can use to highlight their skills. So I wanted (and achieved) a structure like this:

  • /developers - paginated list of our developers
  • /developers/javascript - paginated list of our developers that specialise in JavaScript. This is a taxonomy view.
  • /developers/tomaz-zaman - single view of a developer custom post type.

Note the similarity between the last two items? We need to tell WordPress to use the developers as a slug on both, our custom post type and our custom taxonomy.

After googling for a solution, I was quite surprised that webmaster aren't aware there actually is one and they mostly just accept what WordPress gives them by default.

Let's fix that.

Register a custom post type and a custom taxonomy

Before we can play around with rewriting our taxonomy URLs, we need to setup our entities (feel free to use your existing ones if you have them). I'll use 'developer` custom post type because it's a real use case we needed to implement:

function register_developer_entities() {
    $developer_args = array(
      'public' => true,
      'label'  => 'Developers',
      'rewrite' => array( 'slug' => 'developers' ),
      'taxonomies' => array( 'developer_tag' )
    );
    register_post_type( 'developer', $developer_args );

    $taxonomy_args = array(
      'labels' => array( 'name' => 'Developer Tags' ),
      'show_ui' => true,
      'show_tagcloud' => false,
      'rewrite' => array( 'slug' => 'developers' )
    );
    register_taxonomy( 'developer_tag', array( 'developer' ), $taxonomy_args );
}

add_action( 'init', 'register_developer_entities' );

The code is pretty straightforward and I stripped most of the unnecessary (for the purpose of this tutorial) parts out. There are a couple of things you should notice though:

  • the order we register CPT and Taxonomy. The latter comes last, otherwise it won't work.
  • they both use the same slug (developers)
  • CPT has the taxonomies populated with our custom taxonomy, and vice versa, taxonomy has the CPT as the second parameter of the registration function.

If you try to visit any URLs at this point, chances are you will not able to access one or the other - you'll get 404 (page not found) error, because we haven't yet defined our custom URL rewriting. That happens because you defined two separate entities with the same slug and WordPress doesn't know how to deal with that, effectively overwriting one with the other.

Generate rewrite rules

To reverse that effect, we need to manually append URL for each of our custom terms to the rewrite rules, and this is how it's done (explanation under the code snippet):

function generate_taxonomy_rewrite_rules( $wp_rewrite ) {
  $rules = array();
  $post_types = get_post_types( array( 'name' => 'developer', 'public' => true, '_builtin' => false ), 'objects' );
  $taxonomies = get_taxonomies( array( 'name' => 'developer_tag', 'public' => true, '_builtin' => false ), 'objects' );

  foreach ( $post_types as $post_type ) {
    $post_type_name = $post_type->name; // 'developer'
    $post_type_slug = $post_type->rewrite['slug']; // 'developers'

    foreach ( $taxonomies as $taxonomy ) {
      if ( $taxonomy->object_type[0] == $post_type_name ) {
        $terms = get_categories( array( 'type' => $post_type_name, 'taxonomy' => $taxonomy->name, 'hide_empty' => 0 ) );
        foreach ( $terms as $term ) {
          $rules[$post_type_slug . '/' . $term->slug . '/?$'] = 'index.php?' . $term->taxonomy . '=' . $term->slug;
        }
      }
    }
  }
  $wp_rewrite->rules = $rules + $wp_rewrite->rules;
}
add_action('generate_rewrite_rules', 'generate_taxonomy_rewrite_rules');

Let's investigate what's going on here: First we create an empty array that will hold our custom rules and which we — once populated — append to the default ones at the end of the function.

Then, we retrieve the custom post type and it's matching taxonomy, but this function also supports retrieving as many pairs as you want, hence the outer for-loop.

The inner for-loop though, is where the magic happens; We traverse through all the taxonomies in order to find a match between it and a corresponding post type. Once we do, we get all the terms for that taxonomy (in our case that would be CSS, HTML, Gravity Forms, WooCommerce, etc.) and create a new rewrite rule for each term, so that in our case, /developers/css becomes index.php?developer_tag=css and WordPress resolves our URL properly.

What about template files?

Because we're displaying two different entities under the same URL structure, we need to make sure WordPress renders them correctly; They both need to display the same set of information (taxonomy is in a sense just a filter), which is why you need to create a new template file, with our example that would be taxonomy-developer_tag.php and put the following content in:

<?php

// Find and load the archive template.
locate_template( 'archive-developer.php', true );

It'll just locate our developer archive template file and use that to display all developers that match a particular term.


That's it, you now should have much more user-friendly custom taxonomy URLs!