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.
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
ShibaShake says
What is $event_structure and $place_structure?
Really Web Design 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.
ShibaShake says
Try using %videos% (or whatever rewrite tag you are using for your custom post type) instead of %postname%.
Really Web Design 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!
ShibaShake 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.
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
ShibaShake says
Hello Léon,
What is the permalink structure of your custom post type?
It sounds like a permalink conflict issue.
http://shibashake.com/wordpress-theme/custom-post-type-permalinks-part-2#conflict
Léon says
Hi,
Sorry for late reply.
Structure is: /%ean%/%game%
$wp_rewrite->add_rewrite_tag("%game%", '([^/]+)', "game=");
$wp_rewrite->add_permastruct('game', '/%ean%/%game%', false);
add_filter('post_type_link', 'game_permalink', 10, 3);
%ean% is replaced by a custom taxonomy value. Which works fine.
In the gallery_permalink() function I added:
if ( strpos($permalink, '%ean%') !== false ) {
$terms = wp_get_post_terms( $post_id->ID, 'ean' );
$ean = ( !empty( $terms[0]->name ) && is_numeric( $terms[0]->name ) ) ? $terms[0]->name : '1';
}
ShibaShake says
Yes this is a premalink conflict issue. Try adding a unique keyword to your permalink structure, e.g.
/my-unique-keyword/%ean%/%game%
James 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!
ShibaShake says
Hello James,
One way is to hook into the request filter and then manually insert in the custom post type into the query args. If the custom post type is not specified, then it defaults to ‘post’.
This article has an example of hooking into the request filter –
http://shibashake.com/wordpress-theme/mastering-the-wordpress-loop
I do some of this on my blog-art site which shows archives, recent posts, etc all on my gallery custom post type.
Jurre says
Good article, but I keep wondering what’s the deal with all digital women on this site hmm?
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?
ShibaShake says
You can hook into the ‘request’ filter and add query variables there. I have an example of the request filter here –
http://shibashake.com/wordpress-theme/mastering-the-wordpress-loop
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.
Rutwick 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
ShibaShake says
Probably an issue with permalink conflicts.
http://shibashake.com/wordpress-theme/custom-post-type-permalinks-part-2#conflict
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
ShibaShake 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.
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?
ShibaShake 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.
Manny Fleurmond says
I’m still running into the issue of using %postname% in a custom permastruct.
ShibaShake says
For a custom post type, try adding its own tag, e.g. %gallery% as is described in the tutorial and then use that tag in the permalink structure.