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).
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
Gallery custom post type permalink structure
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.
- tag name – Our custom post type tag name. E.g. %gallery%.
- regex – A regular expression that defines how to match our custom post type name.
- 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.
- name – Name of our custom post type. E.g. gallery.
- struct – Our custom post type permalink structure. E.g.
/galleries/%year%/%monthnum%/%gallery%
- 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 –
- ep_mask – Sets the ep_mask for our custom post type.
http://shibashake.com/wordpress-theme/articles/galleries/2010/06/test-1
This is not what we want, so we set it to false.
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.
Evan 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 –
You can also add the orderby property in your previous ‘request’ filter function.
Epic Alex 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.
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.
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.
Epic Alex says
Excellent, all working now. For some reason though I still needed to add in the rewrite rules for %category%, but hey, it’s all good now!
Evan 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 –
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 –
Evan 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 –
Within the function you can check for the galleries keyword by checking $_SERVER[‘REQUEST_URI’], and then just add post_type into the query.
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.
Evan 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? 😉
Evan says
Absolutely! I just made one. Everything is working beautifully, thank you for all your help.
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 –
If you don’t need to process additional tags – then you don’t even need the post_type_link function.
Andrew 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 –
Lane 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.
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.
Andy Potanin says
Be aware that $post_id passed by the post_type_link filter is actually the post object, not the ID as the name implies.
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
will ensure that $post is always a post object.
Andy Potanin says
Thanks for clearing this up, there are lot of confused developers out there right now trying to figure out the new custom post type functionality.