Custom Permalinks for Custom Post Types in WordPress 3.0+

Update 2010-10-04: See Understanding Rewrite Tags for a somewhat more helpful treatment of the subject.

I’ve been playing around with WordPress 3.0 to create a bug/issue-tracking plugin (i.e., using WP to track bugs, not to track WP bugs (although it could conceivably do that)). More details about the plugin will be forthcoming, but now is as good a time as any to share the solution to one problem I came across.

Creating New Post Types and Taxonomies

When you create a new post type, you use the register_post_type function. When you create a new post type through that function, WP creates a new rewrite rule base on the name you give the post type. In my case, I’ve created an issue post type, so all issues, by default, will have the permalink /issue/%issue%/ (where %issue% is replaced with the post slug). Using the rewrite argument to register_post_type, you can change the beginning of the permalink to something different (e.g., /bugs/%issue%/) should you so desire.

Here’s the full function call I’m using (I’m not guaranteeing that everything here is correct or necessary, but it seems to be working):

register_post_type( 'issue', array(
  'label' => 'Issues',
  'singular_label' => 'Issue',
  'description' => 'A bug to fix, task to complete, or anything else that need to be done',
  'public' => TRUE,
  'publicly_queryable' => TRUE,
  'show_ui' => TRUE,
  'query_var' => TRUE,
  'rewrite' => TRUE,
  'capability_type' => 'post',
  'hierarchical' => FALSE,
  'menu_position' => NULL,
  'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments', 'custom-fields', 'revisions'),
  'menu_position' => 5,
  'rewrite' => array(
    'slug' => 'issue',
    'with_front' => FALSE,
  ),
));

Similarly, you can create a custom taxonomy. In this case, I’ve created a taxonomy for projects, so issues can be assigned to projects. Much like a normal category will have a URL like /category/%category%/, my issue_project taxonomy has URLs matching /issue_project/%issue_project%/, which gives you a page with all issues matching the given project name. You can, once again, use the rewrite argument to give a different path (in my case, I’ve used /project/%issue_project%/).

Here’s the function call for the taxonomy (same caveats as above):

register_taxonomy('issue_project', 'issue', array(
  'label' => 'Projects',
  'singular_label' => 'Project',
  'public' => TRUE,
  'show_tagcloud' => FALSE,
  'hierarchical' => TRUE,
  'query_var' => TRUE,
  'rewrite' => array(
    'slug' => 'project'
  ),
));

Putting Things Together

Now, what if you want to combine these, to show the hierarchy in your URLs (e.g., /project/my-project/issue/horrible-bug/)?

I first attempted to change the slug of the issue post type to project/%issue_project%/issue. That didn’t work. I had URLs that looked like /project/%issue_project%/issue/horrible-bug/; %issue_project% wasn’t being replaced. So I scrapped that idea.

I dug around for a while and found the post_type_link filter. Using that, I could add the project URL to the front of the issue URL when WP was building the permalink for an issue. That gave me the correct URLs, but they would 404.

So I dug around some more through the WP_Rewrite class and realized that I had been wasting a lot of time when I already had the solution: do both.

  1. Change the slug argument to register_post_type to project/%issue_project%/issue
  2. Hook into the post_type_link filter with a callback that replaces %issue_project% with the appropriate project slug.

55 Responses to Custom Permalinks for Custom Post Types in WordPress 3.0+

  1. Hristo says:

    So how did you hook the post_type_link? Can you explain? Thanks!

  2. @Hristo:

    First, you need to create the filter function, e.g.:

    
      function my_post_type_link_filter_function( $post_link, $id = 0, $leavename = FALSE ) {
        if ( strpos('%issue_project%', $post_link) === 'FALSE' ) {
          return $post_link;
        }
        $post = get_post($id);
        if ( !is_object($post) || $post->post_type != 'issue' ) {
          return $post_link;
        }
        $terms = wp_get_object_terms($post->ID, 'issue_project');
        if ( !$terms ) {
          return str_replace('project/%issue_project%/', '', $post_link);
        }
        return str_replace('%issue_project%', $terms[0]->slug, $post_link);
      }
    

    Then tell WP to use it when the post_type_link filter is called:
    add_filter('post_type_link', 'my_post_type_link_filter_function', 1, 3);

  3. Hristo says:

    Jonathan, you are the man! Thanks, works like a charm now! I’ve only changed

    if ( strpos(‘%issue_project%’, $post_link) === ‘FALSE’ )

    to

    if ( strpos(‘%issue_project%’, $post_link) < 0 )

    as the previous wasn't working for me.

    Thanks again!

  4. Hristo says:

    Jonathan,

    do you know which filter should I use to hook the category link and change permalink structure to have nested categories in the permalink? (similar to post_type_link but for category links)? Tried with ‘category_link’ filter but no result..

  5. @Hristo:
    Sorry, I accidentally put quotes around FALSE. It should have been:
    if ( strpos('%issue_project%', $post_link) === FALSE )

    As for changing the permalinks of categories, I’m not sure. I thought category parents would automatically show up in the URL, but if that’s not working, I would have started by trying category_link, too. Perhaps you might need term_link for custom taxonomies. If you get a chance to test it before I do, let me know how it works.

    • Daniel says:

      I’m confused. How did you get this working? The permalink gets changed to the desired url, but I get a 404.

      Also, isn’t strpos( haystack, needle,…) and not the other way around as shown in your code snippet?

  6. Hristo says:

    Hi, it is term_link indeed. However, I couldn’t make the link structure the way I like it.

    I am writing a plugin for my portfolio site and I wanted to migrate my portfolio to a custom post type (currently it is located in the built-in posts). Your solution helped me re-creating the structure for the portfolio post type, but it seems that for custom taxonomies wouldn’t be so easy (portfolio categories in my case).

    I just wanted to have a root taxonomy called projects and sub-categories in it. Sub-categories appear with /projects/category-slug/ as desired, but the root was appearing as /projects/projects/ which I don’t like and decided to remove the first ‘project’ word. I wrote this:

    // Portfolio category
    register_taxonomy(‘portfolio_category’, ‘portfolio’,
    array(
    ‘hierarchical’ => true,
    ‘label’ => ‘?????????’,
    ‘singular_label’ => ‘?????????’,
    ‘public’ => true,
    ‘query_var’ => ‘portfolio_category’,
    ‘rewrite’ => array(‘slug’ => ‘projects/%portfolio_category%’,'with_front’=>false),
    ‘_builtin’ => false
    )
    );

    // Add custom taxonomy link for Portfolio
    add_filter(‘term_link’, ‘portfolio_category_link’, 1, 3);
    function portfolio_category_link($link){
    if ( strpos($link,’%portfolio_category%’) >= 0 ) {
    $slug = substr($link,strpos($link,’%portfolio_category%’)+18,-1);
    $term = get_term_by(‘slug’,$slug,’portfolio_category’);
    if($term->parent>0){
    $link = str_replace(‘%portfolio_category%’,'projects’,$link);
    } else {
    $link = str_replace(‘%portfolio_category%/’,”,$link);
    }
    //return str_replace(‘%category_parent%’,'projects’,$link);
    }
    return $link;
    }

    Sub-categories work like a charm, but the root one returns error 404 :(

    I tried other hacks but without success.

    Finally I got it working, not the way I like but an acceptable one – changing the root custom category slug from ‘projects’ to ‘all’. Now it appears as /projects/all/ – almost good.

    If you find a way to have hierarchy in permalinks for custom taxonomies, please write :)

  7. Brent says:

    Thanks Jonathan. Very handy info.

    If you’re looking for beta testers for your Issue Tracking plugin, drop me an email. I’d be very interested in using something like this!

    You may also like to get in touch with Barry Carlyon one of the WP GSoC students as he is working on a bug tracking plugin as announced here http://wpdevel.wordpress.com/2010/04/26/gsoc-students-announced/

  8. Thanks, Brent. I have an early version of the bug tracking plugin up in the WP plugin repository: http://wordpress.org/extend/plugins/buggypress/

    It could certainly use more features, but it is, at least, minimally usable at this point.

  9. Glance World says:

    Hi, i need to extend term_link with parent categories. i have write the following code but get 404 error.

    register_post_type( ‘issue’, array(
    ‘label’ => ‘Issues’,
    ‘singular_label’ => ‘Issue’,
    ‘description’ => ‘A bug to fix, task to complete, or anything else that need to be done’,
    ‘public’ => TRUE,
    ‘publicly_queryable’ => TRUE,
    ‘show_ui’ => TRUE,
    ‘query_var’ => TRUE,
    ‘rewrite’ => TRUE,
    ‘capability_type’ => ‘post’,
    ‘hierarchical’ => FALSE,
    ‘menu_position’ => NULL,
    ‘supports’ => array(‘title’, ‘editor’, ‘author’, ‘thumbnail’, ‘excerpt’, ‘comments’, ‘custom-fields’, ‘revisions’),
    ‘menu_position’ => 5,
    ‘rewrite’ => array(
    ‘slug’ => ‘project/%issue_project%/issue’,
    ‘with_front’ => FALSE,
    ),
    ));

    register_taxonomy(‘issue_project’, ‘issue’, array(
    ‘label’ => ‘Projects’,
    ‘singular_label’ => ‘Project’,
    ‘public’ => TRUE,
    ‘show_tagcloud’ => FALSE,
    ‘hierarchical’ => TRUE,
    ‘query_var’ => TRUE,
    ‘rewrite’ => array(
    ‘slug’ => ‘project’
    ),
    ));

    function my_post_type_link_filter_function( $post_link, $id = 0, $leavename = FALSE ) {
    if ( strpos(‘%issue_project%’, $post_link) === ‘FALSE’ ) {
    return $post_link;
    }
    $post = get_post($id);
    if ( !is_object($post) || $post->post_type != ‘issue’ ) {
    return $post_link;
    }
    $terms = wp_get_object_terms($post->ID, ‘issue_project’);
    if ( !$terms ) {
    return str_replace(‘project/%issue_project%/’, ”, $post_link);
    }
    $parent_id = $terms[0]->parent;
    if ($terms[0]->parent>0){
    $PSLUGS[] = $terms[0]->slug;

    do {
    $cdata = get_term_by( ‘id’, $parent_id, ‘issue_project’ );
    $PSLUGS[] = $cdata->slug;
    $parent_id = $cdata->parent;
    } while ($parent_id!=0);
    krsort($PSLUGS);
    return str_replace(‘%issue_project%’, implode(‘/’, $PSLUGS), $post_link);
    }
    return str_replace(‘%issue_project%’, $terms[0]->slug, $post_link);
    }

    add_filter(‘post_type_link’, ‘my_post_type_link_filter_function’, 1, 3);

    Can you please tell me what i do wrong?

  10. Dave says:

    Are you able to put this into a txt file? Everything is working except for the custom permalink part with the custom category taxonomy

  11. Granulr says:

    How can I erase the post type slug?

    eg: I have a post type of directory and the url is http://www.mysite.com/directory/california-listings

    I need it to be http://www.mysite.com/california-listings

  12. Wow, thank you so much for this fix. It’s really appreciated! The option to make custom taxonomies part of a custom post type’s permalink should definitely be a part of the WordPress core, in my opinion. Custom post types + custom taxonomies are turning WordPress into a real CMS, but something as seemingly small as not being able to also customize the permalinks for this content is a real blow to that.

    If I haven’t found this blog post, I would’ve had to completely rethink the architecture for a current project – for which the structure of the permalinks are integral.

    Kudos!

  13. Daniel says:

    Thanx for sharing this code. Unfortunately I get a 404 when trying to view my “pages”, what is based on an error in the ‘parse_query’ where the ‘get_page_by_path’-function has not the right ‘post_type’ (default: ‘page’). I’ll look for a fix for that

    • Daniel says:

      Seems like I didn’t get the point right… but still having a 404 :(

    • Daniel says:

      Sooo, what worked for me now is to make sure our post_type is set ‘hierarchical’ => TRUE! No 404 anymore…

  14. Casey says:

    Your article was really helpful for one situation I’m having, but I have a slightly different issue elsewhere:

    I created a custom post type for dates that a class are held.
    The page shows a list of these dates.
    Each date is a link to the particular entry, but I really want to either:

    Not have any dates be links at all (plain text)
    -or-
    Link to an external registration site, not within the WP site (they would all link to eventbrite.com/register/blahblah)

    http://72.18.130.197/~getsexyb/schedule/

    I would really appreciate any ideas!

  15. Pingback: Custom Post Type 404 Horrors Solved | DroidFox

  16. Andy says:

    Hi Jonathan,

    Thank you so much for your tutorial, I was able to get a good grip on the basics of how to create custom post types and was even able to make it work… :-)

    But I was just trying to make one scenario work which I’m never able to and am not sure if your code on the function my_post_type_link_filter_function() is for this purpose.

    That is, I would like to create categories using WP sidebar Posts->Categories and assign my custom posts types to them. For eg., I create CAT1 and SUBCAT11 (ie., under a category under CAT1). I then create a custom post and try to assign it to SUBCAT11, which gives me a 404 error, no matter what I try. Do you think you could help me with a hint here?

    Appreciate your help.

    Best,

    Andy

  17. Pingback: Webnews #12: Greek, Kiss, Tokyo & SEO | Andi Licious' Blogosphäre

  18. Eric lee says:

    thanks Jonathan Brinley, i put your code in functions.php, but it doesn’t work and i have changed “public ” to “funtion”, it works now.
    thanks deeply

  19. Niraj says:

    hi
    I added two taxonomies with the help of “GD CPT tools plugin”
    the 2 taxonomies are courses and location

    i want this to come in my permalink
    but it is not coming
    how can i bring that?
    i already added taxonomies so if i want write this code where should i write in which file?

  20. Dasha says:

    Thanks for the article Jonathan.

    I’m still a bit confused. I have a custom post type called ‘portfolio’. Originally it was without rewrite rule, so the links would be of type ‘my-site.com/portfolio/portfolio-record-slug’. I’ve added the rewrite rules to my ‘portfolio’ post type, so that the link now will be of type ‘my-site.com/my-work/portfolio-record-slug’.

    I used this code:

    _x('Portfolio', 'post type general name'),
    'singular_name' => _x('portfolio', 'post type singular name'),
    'add_new' => _x('Add New', 'portfolio'),
    'add_new_item' => __('Add New portfolio'),
    'edit_item' => __('Edit portfolio'),
    'new_item' => __('New portfolio'),
    'view_item' => __('View portfolio'),
    'search_items' => __('Search portfolio'),
    'not_found' => __('No portfolio found'),
    'not_found_in_trash' => __('No portfolio found in Trash'),
    'parent_item_colon' => ''
    );
    $args = array(
    'labels' => $labels,
    'public' => true,
    'publicly_queryable' => true,
    'show_ui' => true,
    'rewrite' => true,
    'rewrite' => array('slug' => 'my-work', 'with_front' => false),
    'query_var' => true,
    'capability_type' => 'post',
    'hierarchical' => false,
    'show_in_nav_menus' => false,
    'menu_position' => 1000,
    'supports' => array(
    'title',
    'editor',
    'author',
    'thumbnail',
    'excerpt',
    'trackbacks',
    'revisions'
    )
    );
    register_post_type('portfolio',$args);
    }
    ?>

    A page that displays all my work (i.e. all recods of ‘portfolio’ post type) is generated correctly – I see all the records and all the links is of type ‘my-site.com/my-work/portfolio-record-slug’ but when I click on it I’m getting 404 page.

    I got confused by post_type_link filter because my rewrite rules are simple, but I don’t understand what I need to do to make new links work.

    Would really appreciate if you could help me out.

    Many thanks,
    Dasha

  21. Matt Hull says:

    I am hoping someone can help me with this, I have created my custom post type, and registered a taxonomy. The custom page is displaying a list of posts, and archive is working, however the links through to individual posts are not. Here is my code from my functions file: http://pastie.org/1318089, and from within my page: http://pastie.org/1318095
    I am just getting a 404 error page not found.
    Anyone shed any light on why my links wouldn’t be working? I have tried a few of the plugins for this, but still no luck.

  22. Matt Hull says:

    Hi,
    Yeah I have tried flushing the permalinks a number of times but still no links through to individual posts. I have also tried the Custom Post Permalinks Plugin, but the pages dont seem to exist. Can I flush the database or something? or is this still a permalinks problem?

  23. Robert says:

    Hello. Great tutorial, thanks. I’m wondering if there is a work around for a custom post that is not assigned one of the taxonomy terms. For example, I have a ‘blog-post’ type (rewrite is ‘blog’) and a ‘products’ taxonomy. Usually posts will be assigned one product term, however, not necessarily. If a product term has not been assigned, I end up with a URL such as example.com/blog/%products%/my-blog-post-name. In this case I would like it to use example.com/blog/my-blog-post-name.

    Is there a way to make it ignore the whole %products% portion in this case (Both in URL creation and permalink handling)? Or are there other suggestions for how to best handle this? Thanks.

  24. Robert says:

    Quick followup to my question… I had the incorrect $search param in the str_replace method called under if (!terms). The URLs are now being generated correctly. (i.e., if no product term is assigned the URL is example.com/blog/my-blog-post-name.)

    However, when I go to view that URL I am getting a 404. My $wp_query for that page shows that WP interprets the request as looking for a ‘my-blog-post-name’ product term.

    How can I get WP to correctly display this post?

  25. great great post. this was exactly what i was looking for, and worked like a charm

    thanks!

  26. Great article. Very informative. Cheers.

  27. So I was digging in to this code with for my own project and I found that would be cool if there wasnt any terms set for this post, no double / would appear.

    Then I’ve changed the url structure in the post_type to: ‘post_type%tipo%’.

    So when I was doing the filter I would add the / just when I have a set term.

    Cya, and thanks for the piece of code.

    This post have something to do with the discussion:
    http://shibashake.com/wordpress-theme/add-custom-taxonomy-tags-to-your-wordpress-permalinks

  28. Matt says:

    Genius! And yes, this works with WordPress 3.2.

    Thanks!

  29. Alps says:

    Guys – how can I include the post id in the front of the permalink structure, when using a custom post type – because i have thousands and thousands of posts and now the issue is it is starting to slow things down as more and more posts are getting put in….

    What can I do – please help….

  30. Pingback: Custom slug in custom post types, 404 errors | Kasamata

  31. jnz31 says:

    hi jonathan
    thanks for sharing your solution. looks like this is the only place in the interwebs, where to find a custom solution for a custom slug :)
    and i was almost giving up, aksin for help, but i found my solution (thanks to Daniel), so i thought id share this:

    i was looking for a solution, to include the author into my costum post type link.
    this is how i roll:

    function my_post_type_link_filter_function( $post_link, $id = 0, $leavename = FALSE ) {
    if ( strpos('%author%', $post_link) === FALSE ) {
    $author = get_the_author();
    return str_replace('%author%', $author, $post_link);
    }
    }

    add_filter('post_type_link', 'my_post_type_link_filter_function', 1, 3);

    plus you got to make the post type hierarchical

    'hierarchical' => true

    enjoy
    jnz31

    • jnz31 says:

      forgot to include my slug setting

      'rewrite' => array('slug' => '/whatever/%author%')

    • jnz31 says:

      one more question:

      this works fine on the website, but in the cms the linking doesnt work properly,
      i get a url.tld/whatever//post
      any solution on this, or f*§# it..? or did i miss something..?

    • jnz31 says:

      sorry again, found another error in my solution
      instead of

      $author = get_the_author();

      you have to write

      get_the_author_meta( 'user_nicename' );

    • jnz31 says:

      me again with the correct filter function


      function my_post_type_link_filter_function( $post_link, $id = 0, $leavename = FALSE ) {
      if ( strpos('%author%', $post_link) === FALSE ) {
      $post = &get_post($id);
      $author = get_userdata($post->post_author);
      return str_replace('%author%', $author->user_nicename, $post_link);
      }
      }

      with that solution it also works in the backend (after you saved the article!).

  32. mos says:

    ahh.. finally figured it out: had to change the permalink structure from /%postname%/ to /%slug%/%postname%/. hope this dosen’t give me other headaches!

  33. DearSusan says:

    Can you also help me? I’m using the More Types plugin.

    When I create a new post the url will look like:
    mydomain/permalink base/postname

    We can edit “permalink base” in More Types. However, how can we include the %post_id%? I tried but it did not work.

    This is extremely important for me to create unique urls.

    Please let me know if you have the answer.

    Thanks!

  34. Iani says:

    Thanks a lot! Your post + the first comment did the trick.

    However, I have a quick question. How to add a “.html” extension to the link we’re talking about?

    Thanks!

  35. Pingback: custom post type and custom taxonomy permalink | web technical support

  36. Daniel says:

    but… ho can I remove the whole custom post type name or slug from the url, to change from example.com/my-post-type/test-1 to example.com/test-1 ????

    So far I have:
    function my_rewrite() {
    global $wp_rewrite;
    $wp_rewrite->add_permastruct('my-post-type', '%postname%/', false);
    add_rewrite_rule('my-post-type/([0-9]{4})/(.+)/?$', 'index.php?typename=$matches[2]', 'top');
    $wp_rewrite->flush_rules(); // !!!
    }

  37. Daniel says:

    Just in case it help somebody


    add_action('init', 'my_rewrite');
    function my_rewrite() {
    global $wp_rewrite;
    $my_structure = '/%postname%.html';
    $wp_rewrite->add_rewrite_tag("%postname%", '([^/]+)', "my-post-type=");
    $wp_rewrite->add_permastruct('my-post-type', $my_structure, false);
    $wp_rewrite->flush_rules(); // !!!
    }

    It changes example.com/my-post-type/test-1 to example.com/test-1.html

    Warning:
    Note the .html (dot HTML) at the end of the new permalink – just in case it gives you conflicts, also be aware of other things in your installation that may be using the same name as your custom post type, like pages, posts or categories, anything that can be accessed from the same url.

  38. how to show categories with function register_post_type

    my code

    function codex_custom_init() {
    $labels = array(
    'name' => _x('News', 'post type general name'),
    'singular_name' => _x('News', 'post type singular name'),
    'add_new' => _x('Add New', 'News'),
    'add_new_item' => __('Add News'),
    'edit_item' => __('Edit News'),
    'new_item' => __('New News'),
    'all_items' => __('All News'),
    'view_item' => __('View News'),
    'search_items' => __('Search News'),
    'not_found' => __('No News found'),
    'not_found_in_trash' => __('No News found in Trash'),
    'parent_item_colon' => '',
    'menu_name' => 'News',

    );
    $args = array(
    'labels' => $labels,
    'public' => true,
    'publicly_queryable' => true,
    'show_ui' => true,
    'show_in_menu' => true,
    'query_var' => true,
    'map_meta_cap' => true,
    'rewrite' => true,
    'capability_type' => 'post',
    'has_archive' => true,
    'hierarchical' => false,
    'menu_position' => 6,
    'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ),
    'taxonomies' => array('n', 'post_tag'),
    'show_ui' => true,
    );
    register_post_type('n',$args);
    }
    add_action( 'init', 'codex_custom_init' );

    Thanks

  39. If you’re implementing custom plugin code instead of custom post types, it can be a serious pain to use add_rewrite_rule, etc.

    http://gabrielharper.com/blog/2012/09/wordpress-custom-urls-for-plugins/

    Might not be the slickest method, but it’s sooo much easier than fidgeting with WP methods, especially where custom themes tend to break things.

  40. Scott Landes says:

    I just wanted to say.. THANK YOU for this code. Saved many hours of work for me!!!

  41. thanks. this code is useful for my site.

  42. Sajid says:

    so many comments.. I also tried this code thanks for this help but here is the the perfect solution http://wordpress.org/extend/plugins/wp-permastructure/

  43. Thanks for the tutorial. I am actually looking to remove a certain taxonomy base slug ‘coupon’. There are few plugins but no one actually remove ‘coupon’ slug rather than ‘store’, ‘coupon_types’ etc. Any help?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" cssfile="">