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.
Mark says
Hey, there’s a typo in your example code – $post->ID should be $post_id.
ShibaShake says
That is not a typo. It should be $post->ID.
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 🙂
ShibaShake says
Sounds like it should work. Let us know how it goes.
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.
Joe says
Can I hire you on this? Email me if so…
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.
ShibaShake says
Sounds like you want multiple different object types to have the same permalink structure. It is possible, but very messy.
http://shibashake.com/wordpress-theme/custom-post-type-permalinks-part-2#same-permalink
Vexed says
I kinda thought that was going to be the answer, but wanted to make sure i didn’t miss something simple. I guess I’ll get to work, maybe i can find a not so messy way to deal with it.
ShibaShake says
If you find a good solution I would definitely love to see it. So please post us a link or send me some code.
Thanks!
jun says
thanks for this post! very helpful!
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.
Senhor W says
Nice post, now i have a custom post permalink.
Thank you!
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
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 –
Due to time constraints, I am unable to work on external projects.
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 –
Also make sure to flush your rewrite rules when you change permalink structures.
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.