I just found a really nifty new trick for using more flexible permalinks for custom post types in WordPress (v3.0 and later). A bit of a back story here:
At a recent DC PHP beverage subgroup I mentioned to Andrew Nacin (@nacin) that I didn’t know anything about WP_Rewrite (the WordPress API that handles permalinks); he said ‘Good luck with that!’ and I felt obligated to learn it. Later, Aaron Jorbin (@aaronjorbin) made the same challenge.
So I opened my phpxref of WordPress and just sat down with the code, and knew it very well ten hours later. I even understand what the EP Masks do. It’s pretty cool stuff, but not for the faint of heart. (see more beneath the fold)
So recently, I made a “News Releases” custom post type on a site. The default permalink for custom post types is http://www.example.com/post_type/post_name/. For a news feed, I wanted to make the permalinks look a little bit more like blog posts, but with ‘newsroom’ before (i.e.: http://www.example.com/newsroom/year/month/post_name/). So here’s how I did it:
First, in the custom post type registration function, turn rewrite OFF:
register_post_type( 'news', array( 'rewrite' => false, 'public' => true ) );
Next, add a rewrite tag, permastruct, and rewrite rule to handle all the rewriting we could need:
add_rewrite_tag( '%news%', '([^/]+)' ); add_permastruct( 'news', '/newsroom/%year%/%monthnum%/%news%/', true, 1 ); add_rewrite_rule( 'newsroom/?$', 'index.php?post_type=news', 'top' );
This is the meat of the rewriting. I won’t go into the details of how or why this works, since it’s very complex. But it does. I may go into how rewrite works at a later date, but right now I want to share just the snippet.
This is almost the end. All of the previous code gets hooked into init. The next part completes the permalinking:
function jpb_cp_news_permalink( $link, $post, $leavename, $sample ){
if( 'news' != $post->post_type )
return $link;
$rewritecode = array(
'%year%',
'%monthnum%',
'%day%',
'%hour%',
'%minute%',
'%second%',
$leavename? '' : '%postname%',
'%post_id%',
$leavename? '' : '%pagename%',
$leavename? '' : '%news%',
);
$unixtime = strtotime($post->post_date);
$date = explode(' ', date('Y m d H i s', $unixtime));
$replace_array = array(
$date[0],
$date[1],
$date[2],
$date[3],
$date[4],
$date[5],
$post->post_name,
$post->ID,
$post->post_name,
$post->post_name,
);
$path = str_replace($rewritecode, $replace_array, $link);
return $path;
}
This function runs any time you pull a ‘news’ post type permalink and adds the correct information. This will even get you the correct permalink with editable slug on the post edit page. This function hooks into post_type_link. So all together now:
function jpb_cp_custom_permalinks(){
register_post_type( 'news', array(
'rewrite' => false,
'public' => true
) );
add_rewrite_tag( '%news%', '([^/]+)' );
add_permastruct( 'news', '/newsroom/%year%/%monthnum%/%news%/', true, 1 );
add_rewrite_rule( 'newsroom/?$', 'index.php?post_type=news', 'top' );
}
add_action( 'init', 'jpb_cp_custom_permalinks' );
function jpb_cp_news_permalink( $link, $post, $leavename, $sample ){
if( 'news' != $post->post_type )
return $link;
$rewritecode = array(
'%year%',
'%monthnum%',
'%day%',
'%hour%',
'%minute%',
'%second%',
$leavename? '' : '%postname%',
'%post_id%',
$leavename? '' : '%pagename%',
$leavename? '' : '%news%',
);
$unixtime = strtotime($post->post_date);
$date = explode(' ', date('Y m d H i s', $unixtime));
$replace_array = array(
$date[0],
$date[1],
$date[2],
$date[3],
$date[4],
$date[5],
$post->post_name,
$post->ID,
$post->post_name,
$post->post_name,
);
$path = str_replace($rewritecode, $replace_array, $link);
return $path;
}
add_action( 'post_type_link', 'jpb_cp_news_permalink', 10, 4 );
And now you’ve got a custom post type that looks and acts exactly like a blog post!
One final note: after implementing this code, you will have to refresh your permalink settings. To do so, go to Settings -> Permalinks and hit save (you don’t even need to change anything).
EDIT
————————-
So I found a couple of areas where the code I posted could be improved. Namely, the fact that the permalink didn’t create archives of content. Here’s the modified code:
function jpb_cp_custom_permalinks(){
register_post_type( 'news', array(
'rewrite' => array('slug'=>'newsroom'),
'public' => true
) );
add_rewrite_tag( '%news%', '([^/]+)' );
$extra_post_types = get_post_types( array( '_builtin' => false, 'publicly_queryable' => true ) );
if( empty( $extra_post_types ) )
return;
add_rewrite_tag( '%post_type%', '('.implode('|',$extra_post_types).')' );
add_permastruct( 'news', '/%post_type%/%year%/%monthnum%/%news%/', true, 1 );
}
add_action( 'init', 'jpb_cp_custom_permalinks' );
function jpb_cp_news_permalink( $link, $post, $leavename, $sample ){
if( 'news' != $post->post_type )
return $link;
$rewritecode = array(
'%year%',
'%monthnum%',
'%day%',
'%hour%',
'%minute%',
'%second%',
$leavename? '' : '%postname%',
'%post_id%',
'%post_type%',
$leavename? '' : '%pagename%',
$leavename? '' : '%news%',
);
$unixtime = strtotime($post->post_date);
$date = explode(' ', date('Y m d H i s', $unixtime));
$replace_array = array(
$date[0],
$date[1],
$date[2],
$date[3],
$date[4],
$date[5],
$post->post_name,
$post->ID,
$post->post_type,
$post->post_name,
$post->post_name,
);
$path = str_replace($rewritecode, $replace_array, $link);
return $path;
}
add_action( 'post_type_link', 'jpb_cp_news_permalink', 10, 4 );
Note that I am using the rewrite attribute of the post type now. Also, the call to add_rewrite_rule has been removed now that we have a rewrite tag to replace it (%post_type%).
You don’t need to even hit save, actually. Just visit the page.
Nice job working through all of this. A lot of people don’t understand the rewriting engine, or think it is too complex, or alternatively not powerful enough… But when you really dig into it, it’s got quite a kick in terms of both power and spice.
Oh, trust me, it’s very complex and hard to understand for a while. But I’m glad I did it. There’s something very rewarding about working through something like that and coming out on top. I may have to turn this into a plugin that adds extra permalinks options on the permalinks page….
Huh. Interesting bit of knowledge to add: this doesn’t actually set the permalinks exactly like blog posts. For example,
/newsroom/2010/08will give you the August 2010 archives from blog posts. I already have an idea of how to fix this, but will have to post the edit later.Sorry, but where I put all this function? I’m newbie on WordPress world, the truth is that I’m a designer that like to try coding sometimes LOL…
I’m using Custom Type that I created using a plugin, so I didn’t need code for custom type, but to create better permalink I’ll need.
Where I put these php functions?
Thanks!
Hi LeoStorch,
Thanks for stopping by. This code was a first pass at something I’ve made into an actual plugin called Custom Post Permalinks. I strongly suggest that you use that plugin instead of this code. It’ll simplify the process for you.
This almost solves my problem, but I must be going wrong somewhere.
I have a custom post type called “bp_news”, which I used to have slugged as “news”. The problem being, I also have a page called “news” that displays a paginated list of all “bp_news” posts. The pagination did not work, because WordPress thought that news/page/2 was a reference to a news post title. I think your solution here will work.
I have the following code:
/* News posts */
add_action( 'init', 'bp_news_register' );
function bp_news_register() {
register_post_type( 'bp_news',
array(
'labels' => array(
'name' => __( 'News' ),
'singular_name' => __( 'News Item' ),
'add_new' => __('Add News Item'),
'add_new_item' => __('Add News Item'),
'edit' => __('Edit News Item'),
'edit_item' => __('Edit News Item'),
'new_item' => __('New News Item'),
'view' => __('View News Item'),
'view_item' => __('View News Item'),
'search_items' => __('Search News Items'),
'not_found' => __('No news items found'),
'not_found_in_trash' => __('No news items found in Trash')
),
'public' => true,
'show_ui' => true,
'menu_position' => 5,
'_builtin' => false,
'capability_type' => 'post',
'hierarchical' => false,
//'rewrite' => array( 'slug' => 'news-item', 'with_front' => false),
'rewrite' => false,
'supports' => array ('title', 'editor', 'comments', 'trackbacks', 'revisions', 'author', 'excerpt', 'thumbnail'))
);
add_rewrite_tag( '%bp_news%', '([^/]+)' );
add_permastruct( 'bp_news', '/news/%year%/%monthnum%/%news%/', true, 1 );
add_rewrite_rule( 'news/?$', 'index.php?post_type=bp_news', 'top' );
}
add_action( 'post_type_link', 'jpb_cp_news_permalink', 10, 4 );
function jpb_cp_news_permalink( $link, $post, $leavename, $sample )
{
if( 'bp_news' != $post->post_type )
return $link;
$rewritecode = array(
'%year%',
'%monthnum%',
'%day%',
'%hour%',
'%minute%',
'%second%',
$leavename? '' : '%postname%',
'%post_id%',
$leavename? '' : '%pagename%',
$leavename? '' : '%news%',
);
$unixtime = strtotime($post->post_date);
$date = explode(' ', date('Y m d H i s', $unixtime));
$replace_array = array(
$date[0],
$date[1],
$date[2],
$date[3],
$date[4],
$date[5],
$post->post_name,
$post->ID,
$post->post_name,
$post->post_name,
);
$path = str_replace($rewritecode, $replace_array, $link); return $path;
}
Now, the permalinks generated by the loop on the “news” page seems to look okay, but WordPress cannot find a post to match it. The permalink displayed on the admin edit page for my custom “bp_news” type looks like: Permalink: http://www.bassettprovidentia.com/news/2011/01/%news%/
Where do you think I am going wrong?
I have finally nailed the permalink issues thanks to your post. Thank you.