Custom Post Types WordPress 3.0 with template archives

UPDATE: As of WP 3.1, custom post types archive pages are built in! (No doubt in response to this an other similar posts…) You just need to set the new ‘has_archive’ parameter to ‘true’ when registering the post type.

I’ve been playing around with the latest nightly betas of wordpress 3.0, especially the custom post type feature.  It looks great for what I want to do, but doesn’t exactly seem “ready”.  I’ve read various blogs and posts about custom post types trying to get a grips on making this lovely little feature do what I want.  Namely http://kovshenin.com/archives/extending-custom-post-types-in-wordpress-3-0/ and WordPress Codex, and this plugin http://wordpress.org/extend/plugins/cms-press/

I tried reading various tutorials on the web, and nothing really did what I wanted, so I’ve decided to make my own tutorial on how to create custom post types in wordpress 3.0 with archive pages.

The code within the plugin was finally what got me what I wanted.  So if your looking how to use custom post types in WordPress 3.0 and want to have your own archive template file with each of your custom post types then this post is for you.  I really JUST started to look into themeing and code in WordPress, so much of my work is borrowed from the sources listed about, and may not be the optimal way of doing things.  If you know of better/other ways to accomplish this, please pipe up in the comments, we’re all friends here.  I’ll try and explain the steps in detail so noobs like myself can follow along.

Here is what I wanted:

  1. A custom post type, we’ll say called “stories” that allows me to add stories in the wordpress backend.
  2. The URL of that custom post type to be an “archive” page of all the stories e.g. (www.domain.com/stories/)
  3. Each story to have its own structure like so (www.domain.com/stories/my-first-story/)

WordPress does numbers 1 and 3 automatically for you after you register the custom post type in your themes “functions.php” file.  This is done like so:

1st – Register the new Custom Post Type with wordpress

register_post_type('stories', array(
	'label' => __('Stories'),
	'singular_label' => __('Story'),
	'public' => true, // Allows it to be publicly queryable
	'show_ui' => true, // Displays the post time in the Admin Interface
	'_builtin' => false,
	'_edit_link' => 'post.php?post=%d',
	'capability_type' => 'post',
	'hierarchical' => false,
	'rewrite' => array("slug" => "stories"), // the slug for permalinks
	'supports' => array('title','editor','author','custom-fields') // What can this post type do
));

The code above will create a new link in your admin area where you can add the custom post type. If you don’t believe it, go ahead and add it to your functions.php file and check it out!

Number 2 turned out to be a LOT more tricky than I initially believed.  Before I get to the solution that worked for me, I’ll give a bit of background.

When you register a new custom post type in wordpress 3.0 it automatically creates rewrite rules for that post type.  (Which is what makes domain.com/stories/my-cool-story/) possible.  It doesn’t create any rewrite rules which would allow for (domain.com/stories/) to show an archive page.  If you tried to type that in, you would get a 404 not found.

The default rewrite rules that wordpress creates for your custom post types is like so:

[stories/[^/]+/attachment/([^/]+)/?$] => index.php?attachment=$matches[1]
[stories/[^/]+/attachment/([^/]+)/trackback/?$] => index.php?attachment=$matches[1]&tb=1
[stories/[^/]+/attachment/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
[stories/[^/]+/attachment/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
[stories/[^/]+/attachment/([^/]+)/comment-page-([0-9]{1,})/?$] => index.php?attachment=$matches[1]&cpage=$matches[2]
[stories/([^/]+)/trackback/?$] => index.php?stories=$matches[1]&tb=1
[stories/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?stories=$matches[1]&feed=$matches[2]
[stories/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?stories=$matches[1]&feed=$matches[2]
[stories/([^/]+)/page/?([0-9]{1,})/?$] => index.php?stories=$matches[1]&paged=$matches[2]
[stories/([^/]+)(/[0-9]+)?/?$] => index.php?stories=$matches[1]&page=$matches[2]
[stories/[^/]+/([^/]+)/?$] => index.php?attachment=$matches[1]
[stories/[^/]+/([^/]+)/trackback/?$] => index.php?attachment=$matches[1]&tb=1
[stories/[^/]+/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
[stories/[^/]+/([^/]+)/(feed|rdf|rss|rss2|atom)/?$] => index.php?attachment=$matches[1]&feed=$matches[2]
[stories/[^/]+/([^/]+)/comment-page-([0-9]{1,})/?$] => index.php?attachment=$matches[1]&cpage=$matches[2]

The current WordPress beta has the logic built into the templating system to look for a file called single-(custom-post-type).php for the custom post types “singular” pages.  So in my case the url (domain.com/stories/my-cool-story/) would be looking for a template file called “single-stories.php” in my themes folder.  If that file doesn’t exists it will use the regular single file, or eventually fall back on the “index.php” file.  Depends on how your theme is set up.

So the problem is that first, there are no rewrite rules for /stories/ to work.  Second, there is no logic to re-direct /stories/ to a template file which would show an “archive” of my stories.

The Solution.

2nd – Add new rewrite rules

After you’ve registered your custom post type, like I showed earlier in this post, we have to set up some re-write rules for our new shiny post type.  This is done like so.


add_new_rules();
function add_new_rules(){

	global $wp_rewrite;

	$rewrite_rules = $wp_rewrite->generate_rewrite_rules('stories/');
	$rewrite_rules['test/?$'] = 'index.php?paged=1';

	foreach($rewrite_rules as $regex => $redirect)
	{
		if(strpos($redirect, 'attachment=') === false)
			{
				$redirect .= '&post_type=stories';
			}
		if(0 < preg_match_all('@\$([0-9])@', $redirect, $matches))
			{
				for($i = 0; $i < count($matches[0]); $i++)
				{
					$redirect = str_replace($matches[0][$i], '$matches['.$matches[1][$i].']', $redirect);
				}
			}
		$wp_rewrite->add_rule($regex, $redirect, 'top');
	}

}

This bit of code I mostly took from the CMS Press plugin mentioned above. Check it out, it’s pretty sweet.

Which should produce the following NEW rewrite rules which are added to our already default rules that WordPress supplies us.  For a total of 19 rules/ custom post type.  (Someone could possibly speak to how many rules would be “acceptable” for a medium sized website in production, as I’m not really sure what constitutes “to many” rewrite rules when it comes to performance in WP…)

[stories/feed/(feed|rdf|rss|rss2|atom)/?$] => index.php?&feed=$matches[1]&post_type=stories
[stories/(feed|rdf|rss|rss2|atom)/?$] => index.php?&feed=$matches[1]&post_type=stories
[stories/page/?([0-9]{1,})/?$] => index.php?&paged=$matches[1]&post_type=stories
[stories/?$] => index.php?paged=1&post_type=stories

Notice that we added  the variable “post_type=stories” to each of our url’s that we rewrote.  This will allow us to redirect to our custom template in the next step.

Note: I’m not sure really where to hook this function so that it doesn’t make rewrite rules on each page load.  Really we only need to write the rules each time the permastructure changes, or we add a new post type, but I’m not sure how/where to do that.. Any ideas?

If your having troubles getting that rewrite rule to work, go and re-update your permalinks settings page.

3rd – Redirect requests on our new rules to our custom template files

So now we are going to add a hook into the system to over-ride the default templating system, because as mentioned in the background info section, wordpress doesn’t have this built in.

We essentially are checking to see if the variable “post_type=stories” exists in our query vars.  If it does, then we set up the feeds and trackbacks stuff that wordpress does, and we redirect to one of two theme files.

If the “name” variable isn’t in our query vars, then obviously the query is for the archive page, as the name variable only appears when we are requesting a single post.  So we redirect to a template file called “single-stories.php” if the name file does exists, (which is what wordpress would have done normally) and we re-direct to a template file called “single.php” if the name variable doesn’t exist.

Here’s my code:


	add_action("template_redirect",'template_redirect');
	function template_redirect()
	{
		global $wp;
		
		$muley_custom_types = array("stories");

		if (in_array($wp->query_vars["post_type"], $muley_custom_types))
		{
			if ( is_robots() ) :
				do_action('do_robots');
				return;
			elseif ( is_feed() ) :
				do_feed();
				return;
			elseif ( is_trackback() ) :
				include( ABSPATH . 'wp-trackback.php' );
				return;
			elseif($wp->query_vars["name"]):
				include(TEMPLATEPATH . "/single-".$wp->query_vars["post_type"].".php");
				die();
			else:
				include(TEMPLATEPATH . "/".$wp->query_vars["post_type"].".php");
				die();
			endif;
				
		}
	}

That way I can have custom 2 separate custom template files (post-type.php, and post-type-single) for each of my custom post types.

You would need to do that code for each of your custom post types, or write a little function to loop through an array of your custom post types and set them up automatically.

It would be nice if I could query wordpress for my custom post types and have it all be dynamic, but I haven’t looked into seeing if wordpress stores the custom post types in the DB.??

Anyway, that should get you started on hacking around with Custom Post Types.  I think that this should be the DEFAULT way that wordpress handles custom post types.  It seems to me logically that almost everyone is trying to do this by default with their wordpress installations that are running more “CMS” style instead of “blog” style.  Previously most everyone did this same sort of structure with categories and such with the Posts, but this way is much more organized if you ask me.

You can also add custom taxonomies (categories) for each of your custom post types should you so be inclined.  i.e. stories could be divided into fishing stories, hunting stories, hiking stories, etc.  however, we are focusing solely on post types in this tutorial.

Things to note:

-The title tags for your custom post type archives page won’t work.  I haven’t looked into how to fix this yet, as I have no idea how wordpress does titles, let alone for custom post types and custom rewrite rules.??  Any insights would be helpful.

43 Responses to “Custom Post Types WordPress 3.0 with template archives”

  1. wilsmex

    @Granulr

    It sounds like what our trying to do is already built into WP by default. The “pages” feature does this out of the box. If you add a new page, it will show up at mydomain.com/new-page

    Then you don’t even have to set up the new post types.

    Reply
  2. Paul Bearne

    Note: the rewrite rules will not work on IIS as WordPress can’t create IIS rules on the fly.

    Reply
  3. Granulr

    @wilsmex

    Yes I know this works with pages but I need these as custom post types… Let me give you another example.

    I have a post type of “Directory” and a post in that post type “California”. Due to SEO reasons I really need the url to be http://www.mydomain.com/california NOT http://www.mydomain.com/directory/california.

    Ultimately it’s easier for most of my clients if these sections are broken up… These sites are large and it would be nice if my clients don’t have to sort between 30k posts.

    Reply
  4. akella

    Thanks so much for your post! It really helped a lot to start hacking around custom post types.

    Also, before i tried your approach i got one problem. Urls like
    http://yourblog.com/movies/casa-blanca/ (movies is custom post type, casa-blanca – single custom type post)
    just didnt work, i always got 404s.
    I investigated, and it turned out that my default url structure for the posts made a conflict with custom post types urls.
    It was set to /%year%/%monthnum%/%day%/%postname%/. Custom posts permalinks started to work when i changed it to /%postname%/.
    Thought it might help someone who will hack with custom post types.

    Reply
  5. wilsmex

    @ akella
    Man I gotta update this blog to WP3, so I can get nested comments easily.

    True about the /%postname%/, I forgot to mention that in the tutorial. There are several versions/variations that have come up since I wrote this post. Some of the code I’ve got is bloated and slightly out-of-date, but I’m glad I could help. Several plugins now handle this as well.

    It is possibly to set up a custom “permastruct” for a specific post type, and leave your current structure for the rest of your blog, but it is pretty hairy. I found a pretty great tutorial on it, and I’ll post the link if I can find it again..

    @Jake

    I checked out your plugin, and it seems to do pretty much what I’ve got but in a much cleaner way! I would suggest adding the feed URLs as well as adding the functionality to add the post types to default feed in wordpress, as they don’t get added. I’ve also done a quick tutorial on this here http://www.ballyhooblog.com/add-custom-post-types-wordpress-main-feed/

    Nice code in the plugin.

    Reply
  6. khaled hakim

    For those of you who are not to interested in messing around with code to get the desired result: custom post types… You can use this great plug-in for WordPress 3.0 that does the job for you… easy to use, straight forward… and even includes a tutorial video of about 4 minutes length (accessible once you install the plug-in) that explains the already easy to use and intuitive plug-in and how to use it.

    CUSTOM POST TYPE UI – WordPress 3.0 Plug-in
    http://wordpress.org/extend/plugins/custom-post-type-ui/

    Cheers.
    Kman

    Reply
  7. Ballyhoo Blog

    @Khaled

    True there are a plethora of plugins that help with custom post types. However, the purpose of this post is to educate on tinkering with the code, not on how to install a plugin.

    Also, the plugin you posted doesn’t address the issue that this entire post is about. Custom Post Type Archive Template pages…

    Just an FYI for those reading…

    The plugin in Jakes comment does address archives, but doesn’t actually create the post types.. A combination of the the two plugins mentioned would likely work.

    Reply
  8. Richard

    In the rewrite rules code the === should really be ==

    Also after template redirect (if using child themes replace TEMPLATEPATH with STYLESHEETPATH) there is a problem with theme options loading also with correct CSS file. For the moment I have no idea why is that.

    Reply
  9. Richard

    Hey, I think the rewrite rules are a bit incomplete. For instance my theme wants to @import a stylesheet with this url
    http://currentpagepermalink/?mystique=css

    If I go to a normal template and insert the above link into the browser address bar I get a text file with the current style.css content. If I go to a custom post type single page and repeat the action I do not get the css file but instead the current page with missing css format, therefore I think that the rewrite rule for this link does not work properly

    Reply
  10. Manuel

    Man, you have made my day! – But I actually have just found two plugins with which you can add whatever content you want, these are ‘More Fields’ and ‘More Types’ is awesomely simple how you can add the content you want, makes a designer’s life simple. Thank you very much for sharing, look for those plugins in WP plugins search, they will save lots of time. :)

    Reply
  11. Jonathan Brinley

    I think you can replace your template_redirect function with a somewhat simpler function:
    function template_redirect() {
    if ( is_robots() || is_feed() || is_trackback() || is_single() ) {
    return; // run the default action
    }
    global $wp;
    $template = locate_template(array(‘type-‘.$wp->query_vars['post_type'].’.php’));
    if ( $template ) {
    include($template);
    exit;
    }
    }
    add_action(‘template_redirect’, ‘template_redirect’);

    WordPress will automatically handle the other cases (robots, etc.), including single-POST_TYPE.php, and only use your template file for your post type’s archives page if it exists. It should also fix the issue with child themes (haven’t test that, though).

    Reply
  12. Steve

    Allah be praised!
    You have saved my bacon man. The part about “single-(custom-post-type).php” was all I needed.
    Good Karma coming your way.

    Reply
  13. Ballyhoo Blog

    @Jonathon — I’ll have to do an update with your new function. That is much cleaner.

    @Richard — See Jonathans comments.. :)

    @Steve — I’m always up for some Good Karma!

    Reply
  14. Brian

    Andrew,

    This is exactly what I’m looking for with a slight spinoff that hopefully you can help me with. I’ve created a custom post type of calendar where each post is an event. I have a meta box which is the event date. In the permalink, I want to use the meta data for the event date instead of the publish date. So I want: /events/year/month/day/post-name Where year month and day are from the meta data not the publish data.

    I’ve gotten everything to work except the rewriting (I’m getting 404’s). I have a more detailed look on the CSS Tricks forum (http://css-tricks.com/forums/discussion/8980/). If you have any insight, I would be more than happy to compensate you for your time. If interested, email me back at the email address I supplied for this content.

    Reply
  15. Ballyhoo Blog

    @Brian,

    Yes, I believe that is actually possible, though it is quite a bit more work than you have set up. I’ll see if I can dig up the link where I learned how to do it.

    Reply
  16. Brian

    Thanks Andrew. And again, if you’re looking for a bit of contract work and think you could do this, I’m willing to pay. I’m tired of beating my head over it! :D

    Reply
    • Ballyhoo Blog

      @Brian,

      I’ve sent you an email with some code that I was able to get working. It wont query for events say with the URL of /events/year/month/day.

      In order to do that, You’ll need to do a new wp_query object in your theme file for the events post type.

      Reply
  17. Jason

    I hope you can help solve this strangeness.

    If I set permalinks to /%year%/%monthnum%/%postname%/ then I can view single.php single-release.php (my custom post type) but not page.php.

    If I set permalink to just /%postname%/ then I can view page.php and single-release.php but not single.php.

    I’m working on a clean install, no plugins, and I’ve tried just about every tutorial I’ve found including this one.

    So I can refresh the permalink rules (visit the permalink page) and get page.php and single.php to show up. I go to view a custom post and then I get the 404.

    If I go back and refresh the permalink page, now I can view my custom post, but page.php will now throw the 404.

    What could be going on? Are you available to be contracted to sort this out, I’m up against the deadline and haven’t been able to get this working…

    Reply
  18. Jason

    Ballyhoo – I’ve built a custom theme.

    I had it almost working last night. If I deleted out the functions.php first, then refreshed the permalink page, navigated to a page and a post.

    And then pasted back in all the code for my custom post type, now I think the order was go to a page again, go to a post again, and now go to the custom post type link.

    The custom post type will give a 404.

    now refresh the permalink page, now they all start working.

    However, trying it again today, in the last step there after visiting the permalink page to flush the rules, my custom post page single-release.php will work, but as soon as it starts working, page.php gives the 404.

    I can refresh the permalink and page.php starts to work, but now single-release.php gives the 404.

    I can’t seem to get both to work at the same time.

    Reply
  19. Jason

    Good god I think I fixed it!! I added:

    global $wp_rewrite;
    $wp_rewrite->flush_rules();

    To the very end of alll the code I have for registering custom post types and custom taxonomies. PHEW.

    But does that mean rewrite rules are flushing every time a page loads? This site I’m working on can get pretty huge traffic spikes. I’m putting it onto a fast server (burstable to 1 gig of memory) but I can’t have crashing!

    I’m quite new to rewrite rules here, and I keep hearing about putting in rewrite rules so they don’t refresh all the time. How do I know if I’ve used it correctly?

    Reply
  20. grandemou

    Why is so difficult to get this working? Tryied 5 tips from 5 websites and all the same

    WP.org should get onto this, is a clear requierement

    Reply
  21. Jason

    Damn. I discover now that by adding the global $wp_rewrite;
    $wp_rewrite->flush_rules();

    my custom posts now work fine, but the custom taxonomy doesn’t find taxonomy.php … it finds FAIL. (404).

    WHY?!?! OMG. Please someone post a proper tutorial on how to use

    global $wp_rewrite;
    $wp_rewrite->flush_rules();

    when you have custom post types working alongside custom taxonomies.

    I will be so grateful.

    Reply
  22. Jason

    Ok. Solved AT LAST! So it really started bugging me that another project works PERFECT. I have my functions file, no crazy rewrites, just register the post types and taxonomies, save the functions file, hit the permalinks page and voila.

    Now this other project…HOURS down the rabbit hole. And you know what? You can’t have a taxonomy called “year.” Seriously folks. I couldn’t believe it. I changed the taxonomy name to “gear” and touched nothing else and it started working fine.

    Change it back to “year” and it breaks. Which I guess makes sense because /%year%/ is a token maybe?

    Anyhow…thanks to all people who put up tutorials including Ballyhoo.

    Reply
  23. Fannie

    Pretty nice post. I just stumbled upon your blog and wished to say that I have
    truly enjoyed surfing around your blog posts. After all
    I’ll be subscribing to your feed and I hope you write again very soon!

    Reply

Leave a Reply

  • (will not be published)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>