3

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?

2
  • I've left a start on a response breaking down the behaviors you've described. I will double back and edit in a solution for the former issue when you elaborate on your desired outcome - what do you wish to be displayed at mysite.com/wiki/help-topics? A list of wiki-help-topic terms? An archive of all posts with any term from that taxonomy?
    – bosco
    Jun 8, 2021 at 19:25
  • @bosco I was thinking I would show an archive of all posts with any term from that taxonomy
    – rugbert
    Jun 9, 2021 at 20:29

1 Answer 1

5
+50

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.

5
  • Thanks @bosco that explains a lot. To answer your question in the comment above, I want mysite.com/wiki/help-topics to use either my taxonomy-help-topics.php template or even just all archive of all posts with any term from that taxonomy. Not picky but I want to use a specified template file so I can create some sort of "term index" with a feed.
    – rugbert
    Jun 10, 2021 at 14:12
  • @rugbert pardon the delayed response! That's great - all of the posts using the taxonomy is much more direct than a list of terms in the taxonomy. I think mapping that request to a query asking for all posts which don't have a term in an empty term list should work - I'll test and post back here in a few hours
    – bosco
    Jun 10, 2021 at 14:25
  • @rugbert added a solution. It was a little more convoluted than I was hoping, but I think it's pretty solid.
    – bosco
    Jun 10, 2021 at 19:18
  • Take my bounty, brilliant @bosco! This is all very informative and I have a much better understanding on the inner workings of WP routing so this little project I am working on is going to be much better thanks to you.
    – rugbert
    Jun 11, 2021 at 14:23
  • Awesome, thanks! This was a nice refresher for me as well :) . If you really want to dig into WordPress's request routing behaviors, take a look at some of the information collected in the $wp_rewrite global, like $wp_rewrite->wp_rewrite_rules(), as well as a bunch of the information exposed on the $wp global - between the two of them you can see all the rules WordPress will apply against a request URI in order, as well as which one matched the current request and what querystring WordPress transformed the request into. Can be super useful in debugging rewrites!
    – bosco
    Jun 11, 2021 at 18:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.