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

The question:

I think maybe my understanding about rewrites and templates for custom taxonomies and CPTs is wrong and maybe someone here could help me solve my problem and educate me.

So I have a post type, “wiki-doc” and a post type “wiki-news”. Each has been set up with custom slugs:

wiki-doc has its rewrite set to:

'rewrite' => [
    'slug' => 'wiki/documentation',
],

wiki-news has its rewrite set to:

'rewrite' => [
    'slug' => 'wiki/news',
],

And that works how I expect it too. mysite.com/wiki/documentation loads an archive template and template conditional of is_archive() with the the wiki-doc post type.

BUT, I also have a custom taxonomy called wiki-help-topic which has the rewrite of:

'rewrite' => [
    'slug' => 'wiki/help-topics',
],

However this slug doesnt work. mysite.com/wiki/help-topics throws a 404. mysite.com/wiki/help-topics/term-whatever does work however, AND the template conditional is is_tax() which is what I want.

I tried adding this rewrite rule:

    add_rewrite_rule(
        '^wiki/help-topics?$',
        'index.php?taxonomy=wiki-help-topic',
        'top',
    );

But now the page loads is_home() which is weird.

So thats the first issue.

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())

My rules keep using the is_archive() conditional and thus the archive template, for example:

add_rewrite_rule(
  '^wiki/documentation/help-topic/([^/]+)?$',
  'index.php?post_type=wiki-doc&wiki-help-topic=$matches[1]',
  'top',
);

This loads all wiki-doc posts tagged with wiki-help-topic but it uses an archive template and not a taxonomy template.

Is it possible to have this URL use is_tax() instead so that I can use a specific template file for that taxonomy or taxonomy term?

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

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.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment