Custom Post Type Permalinks – Part 2

In Custom Post Type Permalinks – Part 1 we considered how to set different permalink options from the register_post_type function call. Here, we focus on how to create our own permalink structures.

With the register_post_type function, we can easily set our custom post type permalink structure to look like this (assuming custom post type = gallery).

http://shibashake.com/wordpress-theme/gallery/test-1

However, we may want to fully customize our custom object permalink structure, similar to how we can fully customize our post permalink structure. Suppose we want to set our permalink structures as follows –

Post permalink structure

/articles/%postname%

Gallery custom post type permalink structure

/galleries/%year%/%monthnum%/%gallery%

1. Turn Off Default Rewrite Rules

First, we set the rewrite argument to false in our register_post_type function call.

	$args = array(
		'publicly_queryable' => true,
		'query_var' => true,
		'rewrite' => false,
                ...
	); 
	register_post_type('gallery',$args);

After we set the argument to false, our gallery custom post type will no longer use pretty permalinks. Instead, our links will look like this –

http://shibashake.com/wordpress-theme?gallery=test-1

Note – It is also necessary to set query_var to true to enable proper custom post type queries.

2. Add New Custom Post Type Rewrite Rules

To get back our pretty permalinks, we need to add our own %gallery% rewrite tag, and our own gallery perma-structure.

// add to our plugin init function
global $wp_rewrite;
$gallery_structure = '/galleries/%year%/%monthnum%/%gallery%';
$wp_rewrite->add_rewrite_tag("%gallery%", '([^/]+)', "gallery=");
$wp_rewrite->add_permastruct('gallery', $gallery_structure, false);

The add_rewrite_tag function accepts 3 arguments.

  1. tag name – Our custom post type tag name. E.g. %gallery%.
  2. regex – A regular expression that defines how to match our custom post type name.
  3. query -The query variable name to use for our custom post type plus an = at the end. The result of our regular expression above gets appended to the end of our query.

For example, suppose our pretty permalink is –

http://shibashake.com/wordpress-theme/galleries/2010/06/test-1

The result of the regular expression match from %gallery% is test-1. This value gets passed on as a public query to our main blog –

http://shibashake.com/wordpress-theme?gallery=test-1

Later, the query link gets translated back into our original pretty permalink so that our end-users are shielded from this whole process.

The add_permastruct function takes in 4 arguments.

  1. name – Name of our custom post type. E.g. gallery.
  2. struct – Our custom post type permalink structure. E.g.
    /galleries/%year%/%monthnum%/%gallery%
    
  3. with_front – Whether to prepend our blog permalink structure in front of our custom post type permalinks. If we set with_front to true here, our gallery permalinks would look like this –
  4. http://shibashake.com/wordpress-theme/articles/galleries/2010/06/test-1
    

    This is not what we want, so we set it to false.

  5. ep_mask – Sets the ep_mask for our custom post type.

Adding the add_permastruct function changes our gallery object permalinks from

http://shibashake.com/wordpress-theme?gallery=test-1

to

http://shibashake.com/wordpress-theme/galleries/%year%/%monthnum%/test-1

which results in a 404 or file not found page error. This is because the permalink tags %year% and %monthnum% were not properly translated.

3. Translate Custom Post Type Permalink Tags

Finally we need to translate the additional tags in our custom post type permalink. To do this, we hook into the post_type_link filter. The code used in our tag translation function was adapted from the regular WordPress post get_permalink function.

// Add filter to plugin init function
add_filter('post_type_link', 'gallery_permalink', 10, 3);	
// Adapted from get_permalink function in wp-includes/link-template.php
function gallery_permalink($permalink, $post_id, $leavename) {
	$post = get_post($post_id);
	$rewritecode = array(
		'%year%',
		'%monthnum%',
		'%day%',
		'%hour%',
		'%minute%',
		'%second%',
		$leavename? '' : '%postname%',
		'%post_id%',
		'%category%',
		'%author%',
		$leavename? '' : '%pagename%',
	);

	if ( '' != $permalink && !in_array($post->post_status, array('draft', 'pending', 'auto-draft')) ) {
		$unixtime = strtotime($post->post_date);
	
		$category = '';
		if ( strpos($permalink, '%category%') !== false ) {
			$cats = get_the_category($post->ID);
			if ( $cats ) {
				usort($cats, '_usort_terms_by_ID'); // order by ID
				$category = $cats[0]->slug;
				if ( $parent = $cats[0]->parent )
					$category = get_category_parents($parent, false, '/', true) . $category;
			}
			// show default category in permalinks, without
			// having to assign it explicitly
			if ( empty($category) ) {
				$default_category = get_category( get_option( 'default_category' ) );
				$category = is_wp_error( $default_category ) ? '' : $default_category->slug;
			}
		}
	
		$author = '';
		if ( strpos($permalink, '%author%') !== false ) {
			$authordata = get_userdata($post->post_author);
			$author = $authordata->user_nicename;
		}
	
		$date = explode(" ",date('Y m d H i s', $unixtime));
		$rewritereplace =
		array(
			$date[0],
			$date[1],
			$date[2],
			$date[3],
			$date[4],
			$date[5],
			$post->post_name,
			$post->ID,
			$category,
			$author,
			$post->post_name,
		);
		$permalink = str_replace($rewritecode, $rewritereplace, $permalink);
	} else { // if they're not using the fancy permalink option
	}
	return $permalink;
}

We Are Done

Once we translate the additional permalink tags, our gallery permalinks will look like this –

http://shibashake.com/wordpress-theme/galleries/2010/06/test-1

And just like that – we are done!

Permalink Conflicts

A common issue that arises when you create your own permalinks are permalink conflicts.

Permalink conflicts occur when two permalinks share the same regular expression structure.

Note – it is regular expression structure and NOT tag structure.

You can use tags with different and unique sounding names but it will not remove your conflict issue as long as your regular expression structure remains the same.

When permalink conflicts happen, you will usually get a 404 Page Not Found error. This happens because when the WordPress system goes to look for the proper permalink rule to fire, there are multiple ones that match. As a result, the system will only fire the first rule that it sees. Those objects that are tied to all subsequent but duplicate patterns will necessarily return a Page Not Found error because the system is using the wrong permalink rule.

The easiest way to avoid permalink conflict issues is to insert a unique slug into your structure. For example, instead of doing –

/%author%/%gallery%/

Do –

/gallery/%author%/%gallery%/

Adding the unique slug/keyword gallery into your permalink structure ensures that there are no regular expression pattern conflicts.


Is it possible to have multiple objects share the same permalink rule?

Yes, but this can get very messy. One way to do this is to only create a single permalink structure for all of your target objects. Then you must manually resolve which object you want by hooking into the request filter hook.

Related Articles

If you enjoyed this article, please help support our site.

Comments

  1. Maksim says

    Hi, I need help.
    I need url structure: http:/stgnlion.com/rajatieto/horoskoopit/kuukausihoroskooppi/syyskuu/2015/test

    horoskoopit- just word
    kuukausihoroskooppi – post type name
    syyskuu – september on finish
    2015 – year

    so I create rewrite like this: $kuukausihoroskooppi_structure = ‘/horoskoopit/kuukausihoroskooppi/%monthnum%/%year%/%kuukausihoroskooppi%’;

    And I don`t need post_slug in url, but when I remove %kuukausihoroskooppi% from revrite, then this page open like archive.php

    And I need that month be not a number, it should be word so in $date I change code on: $date = explode(” “,date_i18n(‘Y F d H i s’, $unixtime));

    but then this page open like 404 page.

    What`s wrong and can I do url structure like:
    http:/stgnlion.com/rajatieto/horoskoopit/kuukausihoroskooppi/syyskuu/2015
    without slug of post and month with word?

  2. Andhi Irawan says

    Hi,
    Nice tutorial
    I have question, example I have a blog http://blog123.com
    and product web http://productABC.com
    In blog123 has post same name with productABC, like Gadget and Travel
    I want to create a link automatically in blog123.
    In blog123, for post Travel (http://blog123.com/travel), I create Show Product button link that link to http://productABC.com/travel.
    I want that button link automatically via code something like http://[productABC base url]/[title from blog123 post title or attribute or post permalink]

  3. Gerard says

    Is it possible to remove the slug of a custom post type? I would like to use them a lot but with the following URL structure: first the domain, then the postname, without the custom post type slug between the domain and the postname slug.

    Thanks in advance!

  4. Jos Faber says

    Thanks to this awesome post I’ve managed to create a complex rewrite structure with three different custom post types ( ~/brands/brand/model/type ).

    Thanks a gazillion!!

  5. says

    Hi,

    I’ve just used your code to add in the month and year into the permalink structure used for a custom post type that displays Events on my clients website. I’m a novice with functions and PHP, however this was really helpful and worked first time, I’m very happy.

    WordPress version: 3.8.1

    Thank you

    David

  6. says

    Thank you for posting this series, it’s very thorough and the material is great!

    Can you post more information on multiple objects sharing the same rewrite rule? I need my permalink structure to be like the example you used without the unique prefix, like:

    /%author%/%gallery%/

    The problem is that when I remove the unique prefix from your example code then all of my CPTs result in 404 errors. I would really appreciate more details on how to avoid this.

    Thank you again!

  7. Aryan says

    You are great. I was making a plugin for WP and I stucked in rewrite rule, I was searching for this tutorial since past 5 days. Thankyou for writing such amazing post.

  8. Ross says

    Your articles are some of the only good ones out there on this subject, thank you. Do you know how to create a custom permalink structure that will assign a random string of letters/numbers to each post? (For example, like bit.ly/38ejsi6) — Maybe creating a structure that allows /%random% where %random% is an 8 character random string? You may be the only one who can help me on this!

  9. Marco Panichi says

    What if I’ve created a hierarchical custom type?

    The code suggested doesn’t work unfortunally!

    I can access posts only from /my-post and not from /my-parent-post/my-post

  10. says

    How can I create rules to display the custom post like my “standart posts, but add “.html” at the end of permalinks? I need a for only one CPT, so that others stay with the default setting.

  11. says

    Hi, i’m trying to remove the custom post type slug entirely. I’ve used this array but the slug keeps generating ‘rewrite’ => array(‘slug’=>”,’with_front’=>false), is this valid? I found it in the WP forums some said it worked others said it didn’t. How can I create custom posts so that no slug is generated? Thanks

  12. Espen says

    Hello, I’m trying to make a custom_post_type “review” and I want to add those to pages. I want to use pages as parents. I have managed to link up regular pages to the custom type. But the permalinks will not work. I only get 404.

    What I want is for the custom type to integrate with pages like regular posts:

    http://mysite.com/page/review-name
    alternatively:
    http://mysite.com/page/reviews/review-name

    Default the permalinks are: http://mysite.com/reviews/page/review-name but that gives me a 404 – and I don’t want that structure

    Only way to access the review is to type in this URL:
    http:/mysite.com/reivews/review-name

    That works, but I don’t want that either.

    Any tips? I’m close to pulling my hair out 🙂

  13. says

    Thank you Shibashake for this in-depth article.
    I managed to create working custom permalinks to CPT. The issue is that when rewrite set to false in the register_post_type function, than the archive page of the CPT is not being assigned a pretty url.
    i.e. I have cpt event and permalink for a single event may be /event/%year%/%monthnum%/%event% but the archive of all events valid only via example.com/?post_type=event and not with example.com/events.

    If setting the rewrite slug, then each single event post is losign the custom structure.
    How can these 2 can work together?

    Thanks

    • says

      Maor,

      I expect you’ve already found your solution, but I wanted to share how I handled the exact issue you’ve described. The code in this post was exactly what I needed to customize my site URL structure (thank you ShibaShake). However, I too, had 404’s on all of my previously working sitename.com/custom-post-slug/ URL’s.

      To correct this, I added an additional permalink structure and made it unique enough to avoid conflicts. Using the example in this post, it would look something like this:

      $gallery_archive_structure = ‘/anything-unique/%post_type%/’;
      $wp_rewrite->add_rewrite_tag(“%post_type%”, ‘([^/]+)’, “post_type=”);
      $wp_rewrite->add_permastruct(‘post’, $gallery_archive_structure, false);

      In my instance, the links to the archive page(s) are hard-coded in the theme so there was no need to edit the WP generated archive link, but I suppose that’d be possible, too.

      Hope this helps the next person.

  14. says

    I had some problems with a code snippet I created from you example. At last I figured out that the date parameter was the cause of the problem. In your example it states date( ‘Y m d’ … ). I fixed my problem with date( ‘Y n j’ … )

    Hope that can help if someone else runs in to the same issue

  15. says

    Thanks so much for this in depth post. Really saved me on a bad permalink custom post type I had set up. I updated the permalink structure and they all broke. They are fixed now!

  16. says

    There was a question about showing cats and sub-cats in the url, like you can with regular wordpress categories. Of note here is when you register the taxonomy, there is a setting in the rewrite rules to do this.

    Under rewrite, the argument pair ‘hierarchical’ => true, turns on allowing /topic/sub-topic/super-sub-topic/, based on your structure, in the example called topic.


    register_taxonomy('topic', 'gam_qa',
    array(
    'labels' => array(
    'name' => _x( 'Topics', 'taxonomy general name' ),
    'singular_name' => _x( 'Topic', 'taxonomy singular name' ),
    'search_items' => __( 'Search Topics' ),
    'all_items' => __( 'All Topics' ),
    'parent_item' => __( 'Parent Topic' ),
    'parent_item_colon' => __( 'Parent Topic:' ),
    'edit_item' => __( 'Edit Topic' ),
    'update_item' => __( 'Update Topic' ),
    'add_new_item' => __( 'Add New Topic' ),
    'new_item_name' => __( 'New Topic Name' ),
    'menu_name' => __( 'Topics' ),
    ),
    'public' => true,
    'show_ui' => true,
    'hierarchical' => true,
    'query_var' => true,
    'rewrite' => array( 'slug' => 'q-and-a/topic', 'with_front' => true, 'hierarchical' => true ),
    'capabilities' => array(
    'manage_terms' => 'manage_qa_terms',
    'edit_terms' => 'edit_qa_terms',
    'delete_terms' => 'delete_qa_terms',
    'assign_terms' => 'assign_qa_terms'
    )
    )
    );

    Then the function you call from. This is a stripped down version, using only the %topic% tag, which will be the name of your custom taxonomy.

    The code looks for a parent to the topic, and as long as it finds one, adds it to the url.


    function gam_qa_permalinks($permalink, $post_id, $leavename) {

    $post = get_post($post_id);
    $rewrite_code = array(
    '%topic%'
    );

    if ( '' != $permalink && !in_array($post->post_status, array('draft', 'pending', 'auto-draft')) ) {

    if ( strpos($permalink, '%topic%') !== false ) {
    $terms = wp_get_object_terms($post->ID, 'topic');
    if (!is_wp_error($terms) && !empty($terms) && is_object($terms[0])) {
    $topics[] = $terms[0]->slug;

    $parent = $terms[0]->parent;
    while($parent) {
    $term = get_term($parent, 'topic');
    $parent = $term->parent;
    $topics[] = $term->slug;
    }
    $topics = array_reverse($topics);

    $taxonomy_slug = join('/', $topics);
    }else{
    $taxonomy_slug = 'no-topic';
    }
    }else{
    $taxonomy_slug = '';
    }

    $rewrite_replace =
    array(
    $taxonomy_slug
    );
    $permalink = str_replace($rewrite_code, $rewrite_replace, $permalink);
    } else { // if they're not using the fancy permalink option
    }
    return $permalink;
    }

  17. says

    You just saved my sanity. I’ve been fighting with these dang ole permalinks/pagination issues for the last week straight.

    Great stuff, keep it up and thanks!

  18. Amy says

    Very basic error, not sure if I’m being silly. I’ve put the ‘// Add filter to plugin init function’ at the top of functions and then am using a custom post type called features, followed all your steps above.

    I want my structure to be

    features/%year%/%month/%day%/%postname%

    but I’m just getting a 404 error?

    add_action('init', 'feature_register');

    function feature_register() {

    $labels = array(
    'name' => _x('Feature', 'post type general name'),
    'singular_name' => _x('Feature', 'post type singular name'),
    'add_new' => _x('Add New', 'Feature item'),
    'add_new_item' => __('Add New Feature'),
    'edit_item' => __('Edit Feature Item'),
    'new_item' => __('New Feature Item'),
    'view_item' => __('View Feature Item'),
    'search_items' => __('Search Feature'),
    'not_found' => __('Nothing found'),
    'not_found_in_trash' => __('Nothing found in Trash'),
    'parent_item_colon' => ''
    );

    $args = array(
    'labels' => $labels,
    'public' => true,
    'publicly_queryable' => true,
    'show_ui' => true,
    'query_var' => true,
    'rewrite' => false,
    'capability_type' => 'post',
    'hierarchical' => false,
    'has_archive' => true,
    'menu_position' => null,
    'supports' => array('title','editor','thumbnail', 'comments','trackbacks', 'author'),
    'taxonomies' => array('post_tag'),
    );

    register_post_type( 'feature' , $args );
    }

    // add to our plugin init function
    global $wp_rewrite;
    $feature_structure = '/features/%year%/%monthnum%/%day%/%postname%';
    $wp_rewrite->add_rewrite_tag("%feature%", '([^/]+)', "feature=");
    $wp_rewrite->add_permastruct('feature', $feature_structure, false);

    Any help appreciated, thanks

  19. Amanda says

    Thanks for the info! It worked great to change the permalinks on my custom post type, but I am getting a 404 when I try to view the post? Is there something I’m missing? I’ve tried saving the Permalinks Settings Page, but that hasn’t done anything. Thanks in advance!

    • Amanda says

      Sorry, I wrote too soon! I saw you already answered it in the previous comments and that worked for me too. Thanks again for the great info!

  20. Rafal Jaskolski says

    Hi 🙂

    great thanks for this tutorial. It works for me, but….

    when I set rewrite as “%category%” im my post custom type there is conflict with pemalink for standard post.

    How can I solve this?

  21. says

    Hello there, thanks for your job, I have set a permalink structure for my custom type ‘spartiti’ in this way:
    /%year%/%monthnum%/%day%/%postname%/
    but I would like to have my archive for this custom post type in this way:

    mywebsite/spartiti

    Is this possible?
    Thank you

  22. says

    Works great, thanks a billion!!! I modified it slightly for a custom post that I wanted to display like /books/%post_id%/%postname% so I was able to remove much of the code, and it looks like this:


    function BookPermalink( $permalink, $post_id, $leavename )
    {
    $post = get_post( $post_id );

    // Don't rewrite unless it's published
    if ( '' != $permalink && !in_array( $post->post_status, array('draft', 'pending', 'auto-draft') ) )
    {

    // These are the things we'll look for
    $rewrite_old = array(
    '%post_id%',
    $leavename? '' : '%pagename%',
    $leavename? '' : '%postname%'
    );

    // And here we decide what to replace them with
    $rewrite_new = array(
    $post->ID,
    $post->post_name,
    $post->post_name
    );

    // Now do the replacing
    $permalink = str_replace($rewrite_old, $rewrite_new, $permalink);

    }

    return $permalink;
    }

  23. Steven says

    Hi,

    I am having problems with creating this for two CPT,
    $wp_rewrite->add_permastruct(‘event’, $event_structure, false);
    $wp_rewrite->add_permastruct(‘place’, $place_structure, false);

    They seem to cancel each other out, if i comment one out the other will work but not both, do u have any advice?

    Steven

  24. says

    Hey Shibashake, thanks for the awesome post on a topic I can’t find much info on anywhere else. If you’re still checking this post, could you help? I’m having trouble implementing this, as so:

    add_action('init', 'videos_rewrite');

    function videos_rewrite() {
    global $wp_rewrite;
    $wp_rewrite->add_permastruct('videos', '/videos/%year%/%monthnum%/%postname%', false);

    }

    Along with your post_type_link hook. The permalinks change correctly, but all head to 404 pages. Am I doing something wrong? I know the structure is to be slightly different from your own.

    Thanks in advance.

      • says

        Thanks for getting back to me. I’ve attempted:

        function videos_rewrite() {
        global $wp_rewrite;

        $wp_rewrite->add_rewrite_tag("%videos%", '([^/]+)', "p=");
        $wp_rewrite->add_permastruct('videos', '/videos/%year%/%monthnum%/%videos%', false);

        }

        (note the p= rather than videos= as my non-fancy permalinks are http://domain.com/?post_type=videos&p=1234 rather than how you describe. This may be where I’m going wrong?)

        Now that I’ve refreshed permalinks the page redirects to monthly archives, so http://domain.com/videos/2011/09/video-title is actually displaying what is located at http://domain.com/2011/09. Quite frustrating! Any thoughts on my problems, greatly appreciated!

        • says

          Yes p= expects the post id whereas your %videos% rewrite tag is passing it the post name (e.g. video-title) instead. I would suggest using a regular query argument (e.g. videos) rather than ‘p’ since p is the wordpress standard for post id.

  25. Léon says

    Hi,

    Thanks for this great example. However I do have a problem.
    I have the custom post type “Game”. I use the code exactly as above but when this is activated my normal posts and pages won’t work anymore. It says “Page cannot be found”. Any ideas how this is possible?
    I have resaved permalink structure.
    The permalink for posts is /%year%/%postname%

    regards,
    Léon

  26. says

    Hi Shibashake,

    Thanks for your great tutorials! Always super in-depth 🙂

    I was able to follow your tutorial and get this working on a website I’m building. One question I do have though, is how can I make archives work for the cpt? i.e. I would like to display a list of all posts made in 2010 when visiting:
    http://shibashake.com/wordpress-theme/galleries/2010/

    Likewise, http://shibashake.com/wordpress-theme/galleries/2010/06/ would display all posts from June of 2010.

    Thanks!

  27. Manny Fresh says

    Is there a way to set the post_type query var when using your technique? The reason why I ask is because for example if you use a permastruct such as:

    /gallery/%author%/%gallery%/

    WordPress will create another rewrite equivilent to this:

    /gallery/%author%/

    That gives the potential to creating special archive pages but I’d like to pass along the post type as a qv along with that. Is it possible?

  28. egasimus says

    Hey there. This is exactly what I was looking for for a while, and it’s also a good way to get one’s hands dirty with the inner workings of WordPress. However, it doesn’t seem to work for me, and I was hoping that you could help me out.

    In my functions.php, I added this code:
    global $wp_rewrite;
    $nps_structure = '/pesnopoika/%pesnop-song-artist%/%pesnop-song-title%';
    add_rewrite_tag("%pesnop-song-title%", "([^/]+)", "narsam_pesnop_song=");
    add_permastruct("narsam_pesnop_song", $nps_structure, false);

    (narsam_pesnop_song is my custom post type)

    However, it doesn’t seem to really rewrite anything – as I also added:
    add_filter('post_type_link', 'rating_permalink', 10, 3);

    function rating_permalink($permalink, $post_id, $leavename) {
    ...
    if (post type is narsam_pesnop_song) echo $permalink;
    ...
    }

    and this echoed a very much unaltered link – http://mysite/narsam_pesnop_song/the-post-title. So I was wondering why does this happen – perhaps it’s the underscores?

    • egasimus says

      Never you mind, I figured it out. The first snippet oughtta be in a hook that executes early, such as the init hook… That’s what I get for skimming through articles instead of reading them carefully.

      Cheers, and thanks again – without your posts, I probably wouldn’t have ever been able to achieve what I needed to.

  29. says

    Hi… Thanks for the post, this one and the other 2 posts on custom permalinks for custom posts are great!

    I have a doubt. I have set up a custom post ‘companies’, and custom taxos as ‘locations’, ‘industries’, ‘sizes’. Now when I try searching for /companies/ or /companies, it works. But I tried with the taxos as /locations/, /industries/, and it doesn’t seem to work! I have used your code, and trying to do it… Ends up with 404. Please tell me what am I doing wrong. Do I need to do the add tag stuff that you did in your other 2 posts on url rewrite for custom posts?

    Thanks in advance!
    Rutwick

  30. Will says

    Thanks for the post – Can I ask what if I have 2 custom post types that I want to set permalink rules for? – How do assign another function to the ‘post_type_link’ filter?

    Thanks

    • says

      You can add as many functions to a filter as you want by using add_filter . Then, just do a post_type check at the beginning of the function. However, unless you are doing some special permalink processing for each post type, it is probably easiest to handle it all using the same function.

  31. Manny Fleurmond says

    Thank you for your post! I found a weird problem messing around with the code you provided. Mainly, if I tried to use the built in %postname% tag in a permastruct (which would let you edit your post slug when editing a custom post type), I would get some weird 400 Bad Request errors. Wasn’t sure why it did so, though.

    Also in your code, wouldn’t using the global add_rewrite_tag function make more sense than the version built into the wp_rewrite class?

    • says

      add_rewrite_tag calls $wp_rewrite->add_rewrite_tag, but it also does a few additional checks and it adds the rewrite rule query variable (which is always set to be the same as the tagname). Since my query variable is already set in register_post_type, I decided to just directly call $wp_rewrite->add_rewrite_tag. Also, in this case, the query variable need not be the same as the tagname.

  32. homem robô says

    Hi Shibashake, I need your help. I’m trying to do this structure:

    mysite.com/%genre%/cases/%cases%/
    mysite.com/%page%/%subpage%/

    Cases is my custom post type.
    Genre is my custom taxonomy.

    But both genres and pages has the same slug, like, “literature”.

    Example:


    mysite.com/literature/ --> this is a page
    mysite.com/literature/authors -> this is a page, child of literature
    mysite.com/literature/case --> this is a list of cases with custom taxonomy "literature"
    mysite.com/literature/cases/ray-bradbury --> this is a custom post type

    Its possible? When I got the CPT working, all my pages get 404. When I get my pages working, all my CPT get 404…

    If you have some suggestion on this subject, please.

    Thanks!

  33. Dan says

    thanks for a great article. I’m still having problem wrapping my head around permalinks. I’m looking for a hierarchical permalink structure for my custom post type using custom taxonomies the same way you can do it with categories. Is there an article you know about that does this or should I have enough info here to do it myself? Thanks!

  34. evan says

    Hi Shibashake,

    I should first thank you for all the help you gave me while I was building this site. I should have sent you a link as soon as it went live.

    I have another rewrite question which will probably be a snap for you and add to this already wonderfully comprehensive post.

    The question: How do you display custom taxonomy per term archives.

    The example is that I have a taxonomy: venues, and I want to be able to click on a venue and display all the posts with that term. For a specific example if you go to http://metroland.net/events/2011/03/16/sick-puppies-adelitas-way/ you will be able to click on the venue, but its currently giving a 404.

    Again thanks for the help before and I hope you enjoy seeing the product of your great teaching.

  35. says

    Thanks for this guide, I managed to get my permalinks correctly working with custom post types and categories, but not for custom taxonomies. For some reason, even when I successfully replace the %taxonomy% tag, adding a single ‘/’ into this will cause 404, presumably because of the regex. One thing I am curious about is why the regex rewrite causes conflicts. Since the regex ‘([^/]+)’ will return the last captured block in the URL not containing a ‘/’, does this mean that even for regular post types this regex will also attempt to match the last block and then pass it as gallery=something if we do not add a unique prefix? If I had the regular post structure as ‘/%year%/%monthnum%/%postname%’ and also:

    $gallery_structure = ‘/%year%/%monthnum%/%gallery%’;
    $wp_rewrite->add_rewrite_tag(“%gallery%”, ‘([^/]+)’, “gallery=”);
    $wp_rewrite->add_permastruct(‘gallery’, $gallery_structure, false);

    Then what happens? Does it change the permalink tags first with gallery_permalink() and then try to apply the list of regexes it has? So then it won’t be able to distinguish between a regular post and a gallery simply by the name? And so by adding ‘/gallery/’ in front of the the structure add_permastruct() will ensure that all galleries are distinguished behind the scenes, allowing it to make some checks before applying a regex?

    Thanks!

    • ShibaShake says

      WP matches the regular expression for the entire permalink. All the rewrite rules are stored in an associative array the ‘rewrite_rules’ option. Each rule looks like this –

      [gallery/([0-9]{4})/page/?([0-9]{1,})/?$] => index.php?year=$matches[1]&paged=$matches[2] 
      

      The first part contains the regular expression and the second part contains the corresponding query. When a pretty premalink comes in, it is matched with each of these rules, and the first one to match gets translated into a query. If your gallery and post contains the same regular expression structure, then only the first rule that matches will be used.

  36. says

    So this works much better than my class, but doesn’t like WordPress 3.1 and the new has_archive in register_post_type.

    It’s rewriting my base URL of /snippets/ to /2011/02/snippets/. Hmm.

    • ShibaShake says

      Sounds like some of the old rewrite rules may still be around.

      Try flushing the rules, and start with no added rewrite rules. Then slowly add them in one by one.
      A quick way to flush rewrite rules is to go into Settings >> Permalinks and do a Save.

  37. says

    I have to thank you for this. I’ve been using a class to do the same thing, but had issues with permalinks showing proper on the front end. May take a look and compare code bases.

  38. says

    Hi there, I am using a plugin for managing datasets which sets permalinks as http://www.mysite.com/mypage/show=7 Can you point me out how can I change this link structure to pretty permalinks ? Thanx

    • ShibaShake says

      Do you have permalinks set in Settings >> Permalinks?

      If so, then it is likely that the plugin itself does not use pretty permalinks. In this case, it is best to contact the plugin creator, or you can edit the plugin and try to add it in yourself using the instructions outlined above (if the plugin uses custom post types).

  39. says

    Thank you very much for this article. Yours is by far the best explanation of how to add custom permalinks to custom post types. It has been very helpful. 8)

  40. Fab says

    Hi,

    i tested your code and work for me.
    I have only a problem with 1 custom_post that i want with this structure http://mysite.com/%postname%/ (the same permalink that i use for native ‘post’ post_type) but not work. Maybe is a conflict.

    I tried your code changing:

    $gallery_structure = '/galleries/%year%/%monthnum%/%gallery%'

    with this

    $gallery_structure = '%gallery%';

    but not works…

    If i put something before like this

    $gallery_structure = '/foo/%gallery%';

    or this

    $gallery_structure = '/%year%/%gallery%';

    it works..

    The only type of permalink that i cant get is this

    $gallery_structure = '/%gallery%';

    Do you now if it’s possible to get it work?

    Thanks

  41. David Mac says

    I would like to remove the custom post type from the URL, but I understand this can cause conflicts with pages and posts. I would be happy to insert “page” or “post” into the permalinks for pages and posts, to perhaps get around this conflict.

    In other words, my custom post type ‘brands’ would have a nice permalink like this: mysite.com/armani/

    my pages would be mysite.com/page/mypage1/
    and posts would be mysite.com/post/mypost1/

    Is this possible, or have I missed the point somwhere along the line?

    Thank you 🙂

  42. Joe says

    Hey ShibaShake and everyone, i feel like i’m this > – add_rewrite_tag(“%gallery%”, ‘([^/]+)’, “gallery=”);
    $wp_rewrite->add_permastruct(‘gallery’, $gallery_structure, false);

    I’m not using a plugin for this, just trying to get it going from functions.php

    Here is my method:

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

    function calendar_link_filter( $post_link, $id = 0, $leavename = FALSE ) {
    $post = get_post($id);
    if($post->post_type != ‘super_duper’) {
    return $post_link;
    }
    $date = get_post_meta($post->ID,’event_start_date’,true);
    $date = strtotime($date);
    $str = $post_link;
    $str = str_replace(‘%cal_year%’,date(“Y”,$date),$str);
    $str = str_replace(‘%cal_month%’,date(“m”,$date),$str);
    $str = str_replace(‘%cal_day%’,date(“d”,$date),$str);
    return $str;

    }

    add_action( ‘init’, ‘create_my_post_types’ );

    function create_my_post_types() {
    register_post_type( ‘super_duper’,
    array(
    ‘labels’ => array(
    ‘name’ => __( ‘Super Dupers’ ),
    ‘singular_name’ => __( ‘Super Duper’ )
    ),
    ‘rewrite’ => false,
    ‘query_var’ => true,
    ‘hierarchical’ => false,
    ‘show_ui’ => true, // UI in admin panel
    ‘publicly_queryable’ => true,
    ‘public’ => true,
    ‘rewrite’ => array(‘slug’ => ‘events21/%cal_year%/%cal_month%/%cal_day%’),

    )
    );
    flush_rewrite_rules();
    }

    • Joe says

      My post got messed up in the beginning. I was saying how i’m getting a 404 even with using your suggestion of a unique beginning slug. (In my case events21).

      I’m trying to get custom permalinks based on a datepicker in my post for events, as i want their permalink to show the date picked, and NOT reflect the post date like using %year%/%month%/.

      The method works for publishing the permalink, however I can’t view the post without getting a 404.

      Does this have to do with your step 2. “Add New Custom Post Type Rewrite Rules”?

      • ShibaShake says

        You probably need to add rewrite tags for %cal_year%, %cal_month%, %cal_day%. Any tags that are not WordPress standard tags, you need to add rewrite tags for; unless they are automatically added as part of the custom post type or custom taxonomy creation.

  43. Vexed says

    I’m having a terrible time getting my concept to work. I suppose it’s very possible that it’s not doable, but here it is.

    I would like a custom post type of ‘news’.
    I would like to have categories, lets say ‘local’, ‘national’

    I’m looking for the following URL structure.

    http://site.com/local/news/story-name/
    or
    http://site.com/national/news/story-name/

    Which really is just giving me a permalink of /%category%/%type%/%postname%/

    but because I also want the following it is causing issues.

    I would like http://site.com/local/ to be a page as well as http://site.com/local/news/ to also be a page.

    In the same regards I would like http://site.com/national/ to be a page as well as http://site.com/national/news/

    I have been successful at either getting the pages to work and the permalink to the story fails,.. or I can get the permalink to the story to work, but the pages fail.

    Any thoughts?

    Thanks in advance for any help.

  44. Matt says

    This has been a great post and so helpful setting up a custom permalink structure but I’m running into problems when I set this up for multiple custom types

    My two custom post types are reviews & articles

    ideally i’ll have
    Reviews: /%selected_city%/%type_of_review%/%review%/
    Articles: /%type_of_article%/%article%/

    Originally I wrote two “gallery_permalink” type functions for each of my permalinks, creating custom re-write tags for taxonomies associated with each custom type. In the ‘gallery_permalink’ function (I called mine review_permalink & article_permalink) I then created the necessary code to pull the taxonomy term from each post successfully.

    So yeah, this worked great except that whenever I whichever post type was registered first would work but the other type would not work because its trying to use the permalink structure for the wrong custom post type.

    I then tried to consolidate both of the permalink functions which unfortunately did the same thing as before.

    Is there a way to have multiple custom permalink structures for multiple custom post types?

    I feel like I’m so close but I’m going crazy here trying to figure out what’s wrong!!

    Anyway, any help will be appreciated!

    M

    • Matt says

      Oh, I also wanted to indicate…

      The urls are all displaying properly. From what I can gather, the way I executed my taxonomies & custom post types looks correct but it seems that all my custom post types are using the same permalink structure (the first one registered) regardless of the type.

      When I load my ‘articles’ first, all my reviews come up with 404 errors indicating that the post type is an attachment

      GRRR!!!

      • ShibaShake says

        There is likely a conflict in your permalink structures. In particular, permalink structures are resolved using regular expression tests as defined in your add_rewrite_tag function. It does not matter that your permalink structures contain different tags. A conflict will arise if two structures share the same regular expression structure.

        You can just do a quick test and change your permalink structure to –
        /review/%selected_city%/%type_of_review%/%review%/
        /articles/%type_of_article%/%article%/

        By adding /review and /articles you are ensured that your permalink structure will be unique.

        • Matt says

          Yep, adding those static key words definitely fixed it.

          I was hoping to be able to avoid needing the ‘review’ & ‘article’ but it might not be possible, simply because the regExp can match on too many levels.

          Thanks for your help on this!

          M

          • ShibaShake says

            It is possible to have multiple objects share the same permalink structure by hooking into the request filter and then doing conflict resolution there. However, that quickly becomes ugly and messy.

  45. jason says

    I’ve tried so many things to get permalinks working on a site with 3 custom post types. I can always seem to get things to work, but then normal pages (page.php) start giving the 404.

    So I’ve tried to just turn off the permalinks for the main custom post type (release) and use your tutorial…but once putting in the code (changing gallery to release) and putting into the theme’s functions.php file I get:

    Warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, ‘release_permalink’ was given in /home1/artistid/public_html/dtfcanvas/wp-includes/plugin.php on line 166

    when viewing the list of Release custom post types…..any ideas?

    Are you for hire to help me sort this out? The site is due to launch this week….it all works when permalinks are set to default, and I need to keep the structure /%year%/%monthnum%/%postname%/

    so I don’t loose hundreds of facebook likes.

    On another site I’m working on, everything works fine while using just /%postname%/ permalink. On my current site, /%postname%/ will let me see my custom posts, and page.php but then single.php gives me the 404.

    if I switch it to /%year%/%monthnum%/%postname%/ then single.php and my custom post permalinks work, but page.php goes to the 404….

    what in the world is going on!!!!

    • ShibaShake says

      Warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, ‘release_permalink’ was given in /home1/artistid/public_html/dtfcanvas/wp-includes/plugin.php on line 166

      This is likely because you specified release_permalink as the filter function but did not change the name of the function itself. It should look like this –

      add_filter('post_type_link', 'release_permalink', 10, 3);
      function release_permalink($permalink, $post_id, $leavename) {
      	$post = get_post($post_id);
      ...
      }
      

      Are you for hire to help me sort this out?

      Due to time constraints, I am unable to work on external projects.

      if I switch it to /%year%/%monthnum%/%postname%/ then single.php and my custom post permalinks work, but page.php goes to the 404….

      404 errors usually occur when there are conflicts in your permalink structures. For example, if all or some of your custom post types use permalink structures that are similar to existing WordPress object permalink structures. That is why it is usually safest to have a unique slug in your custom post type permalink structure, for example –

      /unique_slug/%release%
      

      Also make sure to flush your rewrite rules when you change permalink structures.

      // add to your plugin deactivation function
      global $wp_rewrite;
      $wp_rewrite->flush_rules();
      

      Here is what I usually do –
      First, turn off all your custom permalinks. Flush rewrite rules. Then make sure that post and page permalinks are working.

      Then just turn on one of your custom permalinks, and make sure it is unique. Make sure that post, page, tag, category, archive, comment page, and all other existing WP permalinks are still working. Then slowly add new permalink rules as necessary.

      Good luck.

  46. says

    So I’ve run into another custom post type rewrite riddle for you.

    The answer seems like it should be very similar to my original question.

    Using the same archive set up (http://shibashake.com/wordpress-theme/galleries/%year%/%monthnum%/%day%/) I want to be able to change the $query string to show posts listed by comment_count instead of post_date

    I looked at the wp_get_archives function, but I’m not sure how to apply a filter to a if statement like that.

    • ShibaShake says

      You can just use the orderby argument –

      query_posts('post_type=gallery&orderby=comment_count');
      

      You can also add the orderby property in your previous ‘request’ filter function.

  47. says

    Hey,

    Thanks for all these tutorials you’ve done on custom post types and custom taxonomies, I’ve found them absolutely invaluable.

    I’ve followed this tutorial through, and have created my permalinks how I want them, which is basically as per normal Posts, /%category%/%postname%/.

    These permalinks appear correctly on the edit screen, but visiting one results in a 404.

    I’ve put the code I’m using in a pastebin: http://wordpress.pastebin.com/0VUz0vCm

    Any ideas what is going wrong? I’ve visited the permalink setting page as some places tell you to do if you are getting a 404.

    Thanks in advance,

    Alex

    • ShibaShake says

      Hi Alex,
      If I understand you correctly you want to set your custom post type permalink to be exactly the same as your post permalink? If that is the case, then you will have to take a different approach. The tutorial above only works if the permalink structures are unique.

      If you want two object types to have the same permalink structure, you will need to hook into the query request function (‘request’) filter and parse your new custom post type in there.

      It is doable but somewhat messy.

      $player_structure = '/%category%/%postname%/';
      $wp_rewrite->add_rewrite_tag("%category%", '([^/]+)', "category=");
      $wp_rewrite->add_permastruct('player_articles', $player_structure, false);
      

      Also, there is no need to add the %category% tag – the category tag is already added as part of WordPress native. You only need to add new tags – for example your new custom post type tag.

      $player_structure = '/player/%category%/%player_articles%/';
      $wp_rewrite->add_rewrite_tag("%player_articles%", '([^/]+)', "player_articles=");
      $wp_rewrite->add_permastruct('player_articles', $player_structure, false);
      

      I added /player/ to the front of the permalink structure so that it is unique. If two permalink structures are identical for two object types then the rewrite rules will be exactly the same and the system will just fire the first one that it sees.

  48. says

    Thank you so much for this post, I’ve read everything out there on custom post types and this cleared up almost everything for me. I still have one piece of functionality left to solve.

    I want to set up archives that work in the same exact way. For example:

    http://shibashake.com/wordpress-theme/galleries/%year%/%monthnum%/%day%/

    Should list the custom posts of that day. This is normal post behavior, but I’m not sure what I should do to set it up for my custom post type.

    • ShibaShake says

      Hmmm, I looked briefly at the wp_get_archives function.

      One way to do what you want is filter the where query argument in the wp_get_archives function call. On line 904 –

      $where = apply_filters('getarchives_where', "WHERE post_type = 'post' AND post_status = 'publish'", $r );
      

      You can just write a filter into the getarchives_where hook and replace post_type = ‘post’ with your own custom post type. For example something like this –

      add_filter('getarchives_where', 'my_custom_archive', 10, 2);
      function my_custom_archive($where, $r) {
         return str_replace("post_type = 'post'","post_type = 'gallery'",$where);
      }
      
      • says

        Oh cool, that makes a lot of sense, but I’m still missing something. If I put a link in as /galleries/2010/07/08/ it calls my normal archives instead of the new archive function. I think I just need another rewrite rule to tell it to pick up on the word “galleries” being in the url, but I’m not sure how to write that.

        (FYI, I do also have normal archives being used on the page as well so I can’t overwrite them)

        • ShibaShake says

          Ahhhh – ok.

          One quick and dirty way to do this is to hook into the query function itself –

          add_filter('request', 'expanded_gallery_archive');  
          

          Within the function you can check for the galleries keyword by checking $_SERVER[‘REQUEST_URI’], and then just add post_type into the query.

          function expanded_gallery_archive($q) {
          	if (isset($q['year']) && (strpos($_SERVER['REQUEST_URI'], '/galleries') !== FALSE)) {
          		$q['post_type'] = 'gallery';
          	}	
          	return $q;
          }
          

          But then you want to make sure that the strpos check is totally unique so that it doesn’t screw up other permalinks that may look similar.

          Not a very elegant solution – but it should point you in the right direction. Let us know how it turns out.

          • says

            Bingo! that did it. Thank you so much.

            Now I’ve got a bonus question, which just appeared.

            How do I create a custom 404 page to display when I accidentally send someone to a date with no custom-posts?

            This is for listing events and I’ll undoubtedly have dates with no events in which case I would want to show a “try another date” page that would be different from the 404 page for the rest of the site.

          • ShibaShake says

            I would look into using the template_redirect action hook. Then within your function you can check for your custom post type archive URL as well as look at the $wp_query global to see if it is a 404 condition.

            Do I get a bonus cup of coffee? 😉

  49. Jason says

    I’m just trying to add ‘.html’ to custom post type URLs (so they match my existing post permalink structure).

    Any ideas how to add .html?

    • ShibaShake says

      Just follow the steps above and add .html to your permalink structure. For example set your custom post type permalink structure to –

      /gallery/%gallery%.html
      

      If you don’t need to process additional tags – then you don’t even need the post_type_link function.

  50. says

    Great writeup!

    I’m trying to get the following custom permalink structure for 1 of my post custom post types:

    domain.com/post-type/%state%/%postname%/

    Where “state” is a custom taxonomy that behave like categories. Each of my post types will only belong to one “state”.

    Then I could hit domain.com/post-type/%state%/, and it would pull up all of those post types in that have the “state” taxonomy.

    Any ideas?

    • ShibaShake says

      Great question by both you and Lane.

      I have not tried adding my own taxonomy tags to the permalink structures, so I can only guess at what would be needed. My guess is that you would need at least two things –

      • Add some code to process the new tag in your equivalent gallery_permalink function. It can be patterned after the %category% tag code.
      • Add a rewrite tag for your taxonomy using the add_rewrite_tag command.
  51. says

    Thank you so much for this. How would you get the category into the permalink? Even better, how would you get a custom taxonomy category in there?

    • ShibaShake says

      Hello Lane,
      The code above will support the %category% tag in the permalink structure.

      Lines 23-38 in the gallery_permalink function deals with assigning categories.

      I have not tried adding in my own taxonomy tags. You will at least need to include similar code (as the category code) to process the new tag in the permalink structure. You may also need to add a rewrite tag.

  52. granulr says

    How can I just remove the post type from the url?

    currently i have http://www.mysite.com/directory/california-listings

    and my client needs it to be http://www.mysite.com/california-listings

    • ShibaShake says

      You can try following the steps above and passing in /%directory% as your post type permalink structure. However, that has a high probability of conflicting with other objects. For example, your regular post objects may have exactly that same structure.

      It is possible to assign the same permalink structure to multiple object types – but this can get messy very quickly. In particular you would want to hook into the main query function (for example through the request filter), and then alter your query in there to properly assign the right queries to the different object types.

    • ShibaShake says

      Great catch Andy.
      There seems to be a small bug (I think) in WP core where they are passing in the post object instead of the ID.
      Doing

      $post = get_post($post_id);
      

      will ensure that $post is always a post object.

Leave a Reply

Your email address will not be published.