Tag Archives: has-archive

WordPress Custom Taxonomy Archive for Custom Post Types

Part of a series of posts on advanced WordPress customization, this article covers the implementation of a WordPress Custom Taxonomy archive for Custom Post Types.

Desired Effect

What we want in the end are URLs that look like example.com/post-type/taxonomy-name/taxonomy-term leading to a listing of Custom Post Type objects. It’s like example.com/blog/tag/tutorial, but with Custom Post Type and Custom Taxonomy instead of the built-in “post” post type and “tag” taxonomy. Posts should reside at example.com/post-type/%postname%/.

Approaches

There are a couple approaches to implementing this functionality in WordPress. I’ve outlined the two potential approaches below, and then explain my solution.

Custom Permastruct, Manual Custom Query

One could use a Custom Field on the Custom Post Type to store the taxonomy name and term in a key-value store. One could then create a new custom rewrite tag via add_rewrite_tag() to allow rewriting of taxonomy-name/taxonomy-term to a GET query variable. Finally, one could write a plugin and/or customize their theme to extract the newly-added query variable and use it to construct a custom SQL query or WP_Query() object. This would hijack The Loop causing it to only return appropriate posts.

Drawbacks, Other Notes: This approach relies on a Custom Field on each Custom Post with the proper name and value. This means if something is wrong there, the post won’t show up properly. There is no unified system to view all the values of that Custom Field across all posts, and no way to automatically rename them if the need arose. One could use the Advanced Custom Fields plugin to ameliorate some of these issues, but it’s not ideal. Furthermore, this system requires heavy customization of the Theme, which makes it much less portable. Not only with the custom query parsing and execution, but you must also write a custom generator for the taxonomy term links and such.

Hijack Existing Functionality in register_post_type() and register_taxonomy()

register_post_type() and register_taxonomy() already make calls to add_permastruct(), add_rewrite_tag(), and add_rewrite_rule(), so why can’t we hijack those to achieve the desired result? It turns out that you can, but it’s not as straightforward as you might expect. Details below.

Drawbacks, Other Notes: This approach utilizes the taxonomy system, and so benefits from all of the functionality and UI already written for it. Furthermore, it does not require such heavy customization of the theme, making it more portable. So far, the only drawback is that I haven’t figured out how to support multiple taxonomies.

Solution

It took a lot of digging, but I finally located most of the solution on the WordPress StackExchange. It relies on a little bit of under-documented functionality in register_post_type() and register_taxonomy() to hijack the behind-the-scenes calls to add_permastruct() and friends and achieve the desired outcome. I documented this solution on the WordPress support forums. The first step is to register the custom taxonomy with a few special modifications to the $args, particularly rewrite:

// Register Custom Taxonomy
function example_taxonomy_init()  {
    // All Labels as usual
    $labels = array(
        'name'                       => 'Example Taxonomy',
        'singular_name'              => 'Example Taxonomy',
        'menu_name'                  => 'Example Taxonomy',
        'all_items'                  => 'All Example Taxonomies',
        'parent_item'                => 'Parent Example Taxonomy',
        'parent_item_colon'          => 'Parent Example Taxonomy:',
        'new_item_name'              => 'New Example Taxonomy Name',
        'add_new_item'               => 'Add New Example Taxonomy',
        'edit_item'                  => 'Edit Example Taxonomy',
        'update_item'                => 'Update Example Taxonomy',
        'separate_items_with_commas' => 'Separate example taxonomies with commas',
        'search_items'               => 'Search example taxonomies',
        'add_or_remove_items'        => 'Add or remove example taxonomies',
        'choose_from_most_used'      => 'Choose from the most used example taxonomies',
    );
    // IMPORTANT!
    $rewrite = array(
        'slug' => 'example-custom-post/example-taxonomy', // this kicks add_permastruct
        'with_front' => false, // we don't want this appearing under /blog/ or something
    );
    $args = array(
        'labels'                     => $labels,
        'hierarchical'               => true, // behave like pages, not posts; I have not tested post-style archives, but they should work
        'public'                     => true,
        'show_ui'                    => true,
        'show_admin_column'          => true,
        'show_in_nav_menus'          => true,
        'show_tagcloud'              => true,
        'rewrite'                    => $rewrite,
    );
    register_taxonomy( 'example-taxonomy', 'example-custom-post', $args ); // we only want this taxonomy to link to our custom post type, nothing else
}
// Hook into the 'init' action
add_action( 'init', 'example_taxonomy_init', 0 );

What this does is hijack the call to add_permastruct() on line 375 of taxonomy.php in the register_post_type() definition which WordPress generates the taxonomy archive page from. We also hijack add_rewrite_tag() on like 374 which creates the rewriting tag that we reference next. slug must be in the form <custom-post-type-name>/<custom-taxonomy-name> where <custom-post-type-name> is the value of the $post_type parameter when you call register_post_type() and <custom-taxonomy-name> is the value of the $taxonomy parameter in register_taxonomy(). This is the call that actually creates the custom taxonomy archive. Then we register our custom post type, with a few modifications to $args, particularly to rewrite and has_archive:

// Register Custom Post Type
function example_custom_post_init() {
    // All Labels as usual
    $labels = array(
        'name'                => 'Example Custom Posts',
        'singular_name'       => 'Example Custom Post',
        'menu_name'           => 'Example Custom Posts',
        'parent_item_colon'   => 'Example Custom Post:',
        'all_items'           => 'All Example Custom Posts',
        'view_item'           => 'View Example Custom Post',
        'add_new_item'        => 'Add New Example Custom Post',
        'add_new'             => 'New Example Custom Post',
        'edit_item'           => 'Edit Example Custom Post',
        'update_item'         => 'Update Example Custom Post',
        'search_items'        => 'Search example custom posts',
        'not_found'           => 'No example custom posts found',
        'not_found_in_trash'  => 'No example custom posts found in Trash'
    );
    // IMPORTANT!
    $rewrite = array(
        'slug'                => 'example-custom-posts/%example-taxonomy%', // keep the % characters
        'with_front'          => false,
    );
    $args = array(
        'label'               => 'example-custom-post',
        'description'         => 'An example custom post type',
        'labels'              => $labels,
        'supports'            => array( 'title', 'editor', 'excerpt', 'thumbnail', ),
        'taxonomies'          => array( 'example_taxonomy' ), // link to our custom taxonomy
        'hierarchical'        => false, // behave like tags, not categories; can be either, but I have not tested hierarchical taxonomy archives
        'public'              => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'show_in_nav_menus'   => true,
        'show_in_admin_bar'   => true,
        'menu_position'       => 5,
        'menu_icon'           => '',
        'can_export'          => true,
        'has_archive'         => 'example-custom-posts', // IMPORTANT!
        'exclude_from_search' => false,
        'publicly_queryable'  => true,
        'capability_type'     => 'page',
        'rewrite'             => $rewrite
    );
    register_post_type( 'example-custom-post', $args );
}
// Hook into the 'init' action
add_action( 'init', 'example_custom_post_init', 0 );

Since we’ve already tied the custom post type and custom taxonomy together in their initialization, we don’t have to call register_taxonomy_for_post_type(). Modifying $rewrite['slug'] allows us to hijack the call to add_rewrite_rule() on line 1309 of post.php with %example-taxonomy%, which is the name of the rewrite tag we created in the call to register_taxonomy() above; this rewrites example.com/example-custom-posts/example-taxonomy/term/post-name to example.com/example-custom-posts/post-name, though we could change that. Importantly, we must set has_archive to something other than true, this creates the regular custom post type archive index (see here). Last, we hijack add_permastruct() again to create the regular index for the custom post type. It took me a while to figure out exactly why and how this worked, and I hope that by documenting it here others may benefit.