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.

57 thoughts on “Custom Permalinks for Custom Post Types in WordPress 3.0+”

  1. @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);

  2. 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!

  3. 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..

  4. @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.

    1. 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?

  5. 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 :)

  6. 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?

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

  8. 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!

  9. 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

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

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

  10. 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!

  11. 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

  12. 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

  13. 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?

  14. 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

  15. 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.

    1. Thank Jonathan, I was having issue with custom post type, I was getting 404. And your comment saved me ;).

  16. 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?

  17. 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.

  18. 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?

  19. 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

  20. 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….

  21. 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

    1. forgot to include my slug setting

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

    2. 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..?

    3. sorry again, found another error in my solution
      instead of

      $author = get_the_author();

      you have to write

      get_the_author_meta( 'user_nicename' );

    4. 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!).

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

  23. 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!

  24. 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!

  25. 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(); // !!!
    }

  26. 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.

  27. 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

  28. 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?

Comments are closed.