Custom taxonomies, with custom rewrites/slug, AND loading a taxonomy archive template from a plugin

Background & Core Functionality

Why a Taxonomy Path Slug Alone Produces a 404

However this slug doesnt work. mysite.com/wiki/help-topics throws a 404.

WordPress does not provide a mechanism for “an archive of taxonomy terms” out of the box – that is, neither the template hierarchy nor the WP_Query/WP_Tax_Query logic support a direct display of terms within a taxonomy – rather both are designed to route requests to queries for post content. I think the decision to omit this behavior is reasonable, as the content that one might wish to display when a request is made for a type of taxonomy varies a lot between use-cases.

This is apparent in a vanilla installation by attempting to visit the address /index.php?taxonomy=category or /category assuming a pretty permalink structure (I will assume the popular /%category%/%postname%/ configuration for the rest of this response).

Because that mechanism does not exist, WordPress does not generate rewrite rules for the base /category URL (visiting that path does not produce the query index.php?taxonomy=category) so the URL path processing falls back on more global rules, and /category is ultimately interpreted as though it were a page slug and – assuming you have not created a page with the slug category – WordPress produces a 404 response as no such page exists.

mysite.com/wiki/help-topics/term-whatever does work however, AND the template conditional is is_tax() which is what I want.

By providing a term that URL path now maps properly into the rewrite rules generated for your taxonomy registration, and WordPress is able to use a complete WP_Tax_Query clause to retrieve posts.

Why a Taxonomy Query Variable Alone Produces the Home/Front Page

I tried adding this rewrite rule: […] But now the page loads is_home() which is weird.

Just as WordPress does not build routes for isolated taxonomy slugs, when parsing query parameters into a query/query vars, WP_Query uses the following logic to build a WP_Tax_Query:

if ( ! empty( $q['taxonomy'] ) && ! empty( $q['term'] ) ) {
  $tax_query[] = array(
    'taxonomy' => $q['taxonomy'],
    'terms'    => array( $q['term'] ),
    'field'    => 'slug',
  );
}

Thus, since /index.php?taxonomy=wiki-help-topic does not include a taxonomy term, it is completely ignored in the composition of the main query, and an empty query produces the home page.

Template Hierarchy Selection Precedence

The second related issue is that I would also like some custom rewrites to show a list of all wiki-doc posts tagged with a wiki-help-topic term, BUT I would like it to use a taxonomy archive (conditional is_tax())

The query composed from the parameters ?wiki-help-topic=some-topic&post_type=wiki-doc should produce a $wp_query reflecting both is_tax() as well as is_post_type_archive() and is_archive() – in fact, if a query is_tax() or is_post_type_archive() it is implicitly is_archive() as well.

You can see the code in WP_Query which determines these properties here.

Counter-intuitively, it looks like the code determining which object is considered “the main queried object” gives precedence to the taxonomy over a post type, but the code which determines which template hierarchy entry point to load gives precedence to a post type archive template over a taxonomy template…

I’m really not sure if that’s a bug, or if the loaded template is often at odds with the queried object.

Solutions

Routing Taxonomy Root Requests to all Posts in Taxonomy

There are a few different ways to handle this, but I think the most useful is to transform requests targeting a taxonomy without terms into ones targeting every term within the taxonomy:

/**
 * Transforms incoming queries specifying a taxonomy slug but no terms into a
 * query targeting all terms within that taxonomy. Note that this handles the
 * specific case of the "taxonomy" query variable being set and the "term"
 * query variable unset - it does not address the case of a taxonomy-specific
 * query variable being present but empty.
 * 
 * @param WP $wp The WP class instance representing the current request.
 **/
function wpse388742_parse_taxonomy_root_request( $wp ) {
  $tax_name      = $wp->query_vars['taxonomy'];

  // Bail out if no taxonomy QV was present, or if the term QV is.
  if( empty( $tax_name ) || isset( $wp->query_vars['term'] ) )
    return;
  
  $tax           = get_taxonomy( $tax_name );
  $tax_query_var = $tax->query_var;

  // Bail out if a tax-specific qv for the specific taxonomy is present.
  if( isset( $wp->query_vars[ $tax_query_var ] ) )
    return;
  
  $tax_term_slugs = get_terms(
    [
      'taxonomy' => $tax_name,
      'fields'   => 'slugs'
    ]
  );

  // Unlike "taxonomy"/"term" QVs, tax-specific QVs can specify an AND/OR list of terms.
  $wp->set_query_var( $tax_query_var, implode( ',', $tax_term_slugs ) );
}

add_action( 'parse_request', 'wpse388742_parse_taxonomy_root_request' );

Performing this transformation so early lets WordPress set up the rest of the request and query as it normally would, meaning the resulting WP_Query object will appropriately reflect is_tax(), is_post_type_archive(), and is_archive() without any further work.

You can then explicitly route requests to the taxonomy slug with a rewrite rule as you have done in your question, or you can use a registered_taxonomy action hook to expose the behavior for all publicly queryable taxonomies:

/**
 * Rewrite requests for the isolated slug of publicly queryable taxonomies to
 * a request specifying the taxonomy without specifying any terms.
 *
 * @param string   $name The name of the registered taxonomy.
 * @param string[] $types An array of object type names which the taxonomy is associated with.
 * @param Array    $tax The WP_Taxonomy object, cast to an associative array.
 **/
function wpse388742_register_tax_root_rewrite( $name, $types, $tax ) {
  if( empty( $tax['publicly_queryable'] ) )
    return;

  $slug = empty( $tax['rewrite'] ) || empty( $tax['rewrite']['slug'] ) ? $name :  $tax['rewrite']['slug'];

  add_rewrite_rule( "^$slug/?$", "index.php?taxonomy=$name", 'top' );
}

add_action( 'registered_taxonomy', 'wpse388742_register_tax_root_rewrite', 10, 3 );

Template Precedence

We can use a simple template_include filter hook to use your taxonomy archive template in those specific conditions – I believe this should work, assuming that the taxonomy term is indeed the queried object (get_taxonomy_template() depends on it being so in order to construct the correct contextual template file paths):

function wpse388741_doc_help_topic_template( $template ) {
  if( is_tax( 'wiki-help-topic' ) && is_post_type_archive( 'wiki-doc' ) )
    return get_taxonomy_template();

  return $template;
}

add_filter( 'template_include', 'wpse388741_doc_help_topic_template' );

This in place, the query described by ?wiki-help-topic=some-topic&post_type=wiki-doc should use the taxonomy archive template instead of the post type archive template.

The behavior could be genericized to apply to all taxonomy/post type pairs by removing the string arguments from the is_ conditionals.

Leave a Comment