Custom taxonomy template for multiple custom post types

When you land in a taxonomy archive page, e.g. the one with url http://yoursite.com/city/london/ WordPress can’t know if you want to see London’s Hotels, Gym of Supermarkets: you have to inform it.

How inform WordPress? By url. So, if your url appear like http://yoursite.com/city/london/show/gym now WordPress understand you want to see only gyms…

How to get that results? What you need is an endpoint:

function my_city_endpoint() {
    add_rewrite_endpoint( 'show', EP_ALL );
}
add_action( 'init', 'my_city_endpoint' );

In this way when you visit an url like the one posted above, WordPress set a query var 'show' that contain your post type, and you can make use of it hooking pre_get_posts action hook.

function my_city_mod_query( $query ) {
    if ( is_main_query() && ! is_admin() && is_tax('city') ) {
        $post_type = $query->get('show');
        if ($post_type) $query->set('post_type', $post_type );
    }
}
add_action( 'pre_get_posts', 'my_city_mod_query' );

Now your the taxonomy archive will show only the post type you set in the url via the endpoint.

If you want to change also the html displayed, you have to change also the template file.

Regarding sidebars you can register 4 sidebar called ‘sidebar-city’ used when no type is passed via url, and three other sidebars called 'sidebar-city-gym', 'sidebar-city-hotel' and 'sidebar-city-supermarket'.

Then in template you can use a condition to show the right sidebar:

$type = get_query_var('show');
$type = ( empty($type) ) ? '' : '-' . $type;
dynamic_sidebar( 'sidebar-city' . $type );

If you even want to customize the way the single item in the loop is displayed (the htnl markup) you can use get_template_part and create 3 different template files for the single post type, so in your taxonomy-city.php you’ll have something like

$type = get_query_var('show');

while( have_posts() ) : the_post();

 if ( empty( $type ) ) { // no type is passed in the url, use standard markup
    ?>
    <h3><?php the_title(); ?></h3>
    <p><?php the_content('continue reading...'); ?></p>
    <?php
 } else {
    get_template_part('city-item', $type);
 }

endwhile;

Using this code, if no type is passed in the url, the template use stardard markup, on the countrary if a type is passed in the url, the template file require one of these files: 'city-item-gym.php', 'city-item-hotel.php' and 'city-item-supermarket.php' depending on type required, so you can full customize the markup.

Of course, if the markup change from type to type is little, you can use some if or switch statements to change what is outputted.

Now you need only last thing: generate the right url in the right page.

You need a filter that should be fired only in post single view for the cpt involved.

add_action('template_redirect', 'maybe_change_term_url');

function maybe_change_term_url() {
   if ( is_single() ) {
     $cpts = array('gym','hotel','supermarket');
     if ( in_array(get_post_type(), $cpts) ) {
       add_filter('term_link', 'change_term_link', 10, 3);
     }
   }
}

function change_term_link( $link, $term, $taxonomy ) {
    $cpts = array('gym','hotel','supermarket');
    $type = get_post_type();
    if ( in_array($type, $cpts) && $taxonomy == 'city' ) {
       return trailingslashit($link) . 'show/' .  $type;
    }
    return $link;
}

As you can see I’ve used template_redirect hook to run a function that check the current page, and if in single post for one of the involved CPTs set the filter using 'term_link' filter, in this way the part '/show/gym', '/show/hotel' or ‘/show/supermarket' is automatically added to links and everything go in right place.

Please note that all the code here is completely untested and wrote here without any syntax highlight so probably you’ll found some errors or typo…