64

I've been combing this site and google for the answer and I've come up completely empty. Basically I want to do exactly what this post asks, but I need a hierarchical taxonomy. The answer given in that post works great, but only with a single level taxonomy. Is it possible to do what I want? I've tried a million things, but none work, at best I can get the right permalinks but they come up 404.

To visually illustrate what I want:

/basename/ - ideally a page, but I think this will cause a permalink collision    
/basename/top-cat/ - top parent custom taxonomy archive    
/basename/top-cat/child-cat/ - child cat custom taxonomy archive     
/basename/top-cat/child-cat/grandchild-cat/ - grandchild cat custom taxonomy archive    
/basename/top-cat/child-cat/grandchild-cat/post-name/ - my custom post type post

You can do this fine with the built in posts and categories, how do you do it with custom taxonomies and custom post types? I know that you need to use 'rewrite' => array( 'slug' => 'tax-name', 'with_front' => true, 'hierarchical' => true ), to get hierarchical slugs, which works fine on the archive pages, but the custom post type posts come up 404. If I remove the 'hierarchical' => true part then the posts work, but I lose the hierarchical urls (only /basename/grandchild-cat/post-name/ works).

So, any solutions? Thank you so much, this has been driving me nuts for about 3 weeks now.

4 Answers 4

78

After combining a bunch of pieces of other answers I got it working! So here's the solution for those of you who are struggling with this too:

This post and this one helped me out some, so thanks to those guys.

Note, all this code, plus your initial custom post type and taxonomy registration code goes in your functions.php file.

First get your slugs right when defining your custom post types and taxonomies: for the custom post type it should be basename/%taxonomy_name% and the slug for your taxonomy should be just basename. Don't forget to also add 'hierarchical' => true to the taxonomy rewrite array to get nested terms in your url. Also make sure query_var is set to true in both cases.

You need to add a new rewrite rule so WordPress knows how to interpret your url structure. In my case the custom post type part of the uri will always be the 5th uri segment, so I defined my match rule accordingly. Note that you may have to change this if you use more or less uri segments. If you'll have varying levels of nested terms then you'll need to write a function to check whether the the last uri segment is a custom post type or a taxonomy term to know which rule to add (ask me if you need help on that).

add_filter('rewrite_rules_array', 'mmp_rewrite_rules');
function mmp_rewrite_rules($rules) {
    $newRules  = array();
    $newRules['basename/(.+)/(.+)/(.+)/(.+)/?$'] = 'index.php?custom_post_type_name=$matches[4]'; // my custom structure will always have the post name as the 5th uri segment
    $newRules['basename/(.+)/?$']                = 'index.php?taxonomy_name=$matches[1]'; 

    return array_merge($newRules, $rules);
}

Then you need to add this code to let workpress how to handle %taxonomy_name% in your custom post type rewrite slug structure:

function filter_post_type_link($link, $post)
{
    if ($post->post_type != 'custom_post_type_name')
        return $link;

    if ($cats = get_the_terms($post->ID, 'taxonomy_name'))
    {
        $link = str_replace('%taxonomy_name%', get_taxonomy_parents(array_pop($cats)->term_id, 'taxonomy_name', false, '/', true), $link); // see custom function defined below
    }
    return $link;
}
add_filter('post_type_link', 'filter_post_type_link', 10, 2);

I created a custom function based on Wordpress's own get_category_parents:

// my own function to do what get_category_parents does for other taxonomies
function get_taxonomy_parents($id, $taxonomy, $link = false, $separator = '/', $nicename = false, $visited = array()) {    
    $chain = '';   
    $parent = &get_term($id, $taxonomy);

    if (is_wp_error($parent)) {
        return $parent;
    }

    if ($nicename)    
        $name = $parent -> slug;        
else    
        $name = $parent -> name;

    if ($parent -> parent && ($parent -> parent != $parent -> term_id) && !in_array($parent -> parent, $visited)) {    
        $visited[] = $parent -> parent;    
        $chain .= get_taxonomy_parents($parent -> parent, $taxonomy, $link, $separator, $nicename, $visited);

    }

    if ($link) {
        // nothing, can't get this working :(
    } else    
        $chain .= $name . $separator;    
    return $chain;    
}

Then you need to flush your permalinks (just load your permalinks settings page).

Now everything 'should' work hopefully! Go make a bunch of taxonomy terms and nest them correctly, then make some custom post type posts and categorize them correctly. You can also make a page with the slug basename, and everything should work the way I specified in my question. You may want to create some custom taxonomy archive pages to control how they look and add some kind of taxonomy widget plugin to show your nested categories in the sidebar.

Hope that helps you!

10
  • Fixed- see question. Jeff, thanks for your explanation! But might it be possible for you to post the custom posttype / taxonomy build? So i can see what i am doing wrong ? Would be realy thankfull!
    – Reitze Bos
    Commented Mar 28, 2012 at 9:26
  • Hey Jeff, thanks for your answer! I'm almost there, after 4 hours fighting with this. My only problem is that I get a double slash before the post name (as this: portfolio/diseno-grafico-2/logo//alquimia-sonora/ ) Can you help me?
    – Cmorales
    Commented Apr 7, 2012 at 19:50
  • 1
    @Cmorales, Not sure without seeing your code, but look for a place where you manually define a slash before your post name, like maybe the cpt slug registration or in the filter_post_type function? If you can't track down where you're adding the extra slash then you could just strip the last character from the returned value of the get_taxonomy_parents function called in filter_post_type_link, because that will leave you with a trailing slash, which is the first of the double. Good luck.
    – Jeff
    Commented Apr 9, 2012 at 15:32
  • 1
    "If you'll have varying levels of nested terms then you'll need to write a function to check whether the the last uri segment is a custom post type or a taxonomy term to know which rule to add (ask me if you need help on that)..." Well, can you help me, please? :)
    – Cmorales
    Commented May 22, 2013 at 22:47
  • 2
    ^ that's exactly what i needed. anybody know how to do it? see my question here: wordpress.stackexchange.com/questions/102246/… Commented Jun 7, 2013 at 14:55
3

Take a look at this plugin (now on github). It provides hierarchical URLs for categories, but you can easily adapt to any taxonomy.

The rule creation follows a recursive function.

1
  • 4
    This plugin no longer is available.
    – Enkode
    Commented Dec 5, 2018 at 22:34
1

For dealing with varying level of nesting,

/basename/top-cat/ -> Top Cat Archive
/basename/top-cat/post-name/ -> Post in Top Cat
/basename/top-cat/child-cat/ -> Child Cat Archive
/basename/top-cat/child-cat/post-name/ -> Post in Child Cat

I ended up using Jeff's solution without the rewrite_rules_array filter. Instead I used the request filter to check if the last url part is a valid postname and add it to the query_vars.

eg.

function vk_query_vars($qvars){
    if(is_admin()) return $qvars;
    $custom_taxonomy = 'product_category';
    if(array_key_exists($custom_taxonomy, $qvars)){
        $custom_post_type = 'product';


        $pathParts = explode('/', $qvars[$custom_taxonomy]);
        $numParts = sizeof($pathParts);

        $lastPart = array_pop($pathParts);
        $post = get_page_by_path($lastPart, OBJECT, $custom_post_type);
        if( $post && !is_wp_error($post) ){
            $qvars['p'] = $post->ID;
            $qvars['post_type'] = $custom_post_type;
        }
    }
    return $qvars;
}
add_filter('request', 'vk_query_vars');
0

Here is my take on Jeff's solution. The rewrite_rules_array may not be needed depending on what your application is. This simplifies the filter_post_type_link and does not require any additional functions. It uses the built-in get_term_parents_list()

function PLUGIN_filter_post_type_link($post_link, $post)
{
    $postTypes = ['CUSTOM-POST-TYPE'];
    $taxonomy = 'taxonomy';
    if (in_array($post->post_type, $postTypes, $strict=true)) {
        if ($terms = get_the_terms($post->ID, $taxonomy)
        ) {
            $args = [
                'format' => 'slug',
                'separator' => '/',
                'link' => false,
                'inclusive' => true,
            ];

            return str_replace(
                "%{$taxonomy}%",
                rtrim(
                    get_term_parents_list($terms[0]->term_id, $taxonomy, $args),
                    "/"
                ),
                $post_link);
        }
        return str_replace("%{$taxonomy}%", '', $post_link);
    }
    return $post_link;
}

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