Smarter Custom Post Types in WordPress 3.0

NOTE: I’ve moved this into its own project page, where you can now find the down­load link and leave com­ments. This post will no longer be updated and com­ments are now off.

Word­Press 3.0 is almost ready for prime-time, and with it full sup­port for cus­tom post types. You can read about the tech­ni­cal details of adding a cus­tom post type, or why you should use them. The WP team have done a great job with bring­ing this func­tion­al­ity into the upcom­ing 3.0 release, but one thing that’s puz­zled me is (cur­rent) lack of good sup­port for 1) cus­tom URLs for that par­tic­u­lar post type only (some­thing like http://yourdomain.com/movies ) and 2) cus­tom tem­plates for that post type. (I’ll be using this to move port­fo­lio items to a “port­fo­lio” post type.)

But, that’s noth­ing a lit­tle code can’t fix. With the helper class I’ve made, the fol­low­ing is possible:

  1. Cus­tom URLs for a land­ing page for your post type, with full pag­i­na­tion & feed sup­port. (eg http://yourdomain.com/movies/, http://yourdomain.com/movies/page/2/, http://yourdomain.com/movies/feed/ )
  2. Cus­tom land­ing page tem­plates: if you reg­is­tered “movie” as your post type, you can use movie/index.php or movie.php in your theme direc­tory (falls back to index.php if they don’t exist)
  3. Cus­tom sin­gle page tem­plates:  WP already looks for single-movie.php (and falls back to single.php). This func­tion allows you to use movie/single.php — great along­side movie/index.php for bet­ter theme organization.
  4. adds classes to body_class() and post_class() for that post type.

Usage

After includ­ing the SD_Register_Post_Type class and the helper func­tion sd_register_post_type(), all you need in your functions.php file (or wher­ever you choose to run this) is the fol­low­ing line:

sd_register_post_type( 'movie' );

That’s it. Call it again with a new argu­ment for another post type. Repeat as many times as you need post types. I’ve set good default argu­ments to pass to register_post_type(), but you can over­ride them with your own. (Read more about the $args here, here and here.)

sd_register_post_type( 'movie', $args_array );

Also, gram­mar pedants know that adding an “s” suf­fix is not appro­pri­ate for all plu­rals. In the case of post type “movie”, our URL land­ing struc­ture is http://yourdomain.com/movies, which works fine. But, for irreg­u­lar plu­rals, the func­tion accepts an optional third argument:

sd_register_post_type( 'person', $args_array, 'people' );

The above URL struc­ture would then be http://yourdomain.com/people for the post_type “person”.

One note that’s very, very impor­tant: your cus­tom URLs won’t work until you go to Options → Perma­link in wp-admin and re-save your cur­rent URL struc­ture. This will flush WP’s cur­rent URL struc­ture and add our new rewrite rules. This is com­pu­ta­tion­ally expen­sive and you don’t want it hap­pen­ing every time, which is why I’m leav­ing it as a man­ual oper­a­tion. You’ll only need to do it once (or after chang­ing the third plural argument).

42 Comments

  1. Paul
    Posted April 20, 2010 at 10:50 am | Permalink

    Really great, thanks, I have been fol­low­ing cus­tom post types with excite­ment and have read a few of the posts made so far, but yours dug-in a lit­tle deeper. Great.

    I have a ques­tion about feeds. Am I miss­ing some­thing, or do we have to do some extra work to get feeds work­ing for cus­tom post types?

  2. Matt
    Posted April 20, 2010 at 11:25 am | Permalink

    @Paul Feeds are a good idea. Didn’t even think about it. I’ll have to put that in.

  3. Paul
    Posted April 20, 2010 at 11:40 am | Permalink

    That’d be amaz­ing, I’d love to see it. From what I’ve read, the feeds are nei­ther gen­er­ated for the cus­tom post type, nor are they included in your sites’ main feed. That’s a shame, as both are impor­tant options in my opinion.

    Paul

  4. Matt
    Posted April 20, 2010 at 12:00 pm | Permalink

    @Paul Feeds are in there now. This still won’t include your cus­tom post type in the main feed, but I’m sure some­one out there already has a solu­tion for that.

  5. Paul
    Posted April 20, 2010 at 12:32 pm | Permalink

    Great, looks good.

    There’s not a lot of info on get­ting the cus­tom post types into the main feed, but I did find this one: http://justintadlock.com/archives/2010/02/02/showing-custom-post-types-on-your-home-blog-page

  6. Posted April 20, 2010 at 1:51 pm | Permalink

    I’m pretty sure most of this is already in core — end­points, body/post classes, and single-$post_type.php templates.

  7. Matt
    Posted April 20, 2010 at 2:21 pm | Permalink

    @Nacin Thanks for stop­ping by. single-$post_type.php tem­plate are sup­ported, as are post classes. I’ll remove those as they’re redundant.

    How­ever, I double-checked and there’s def­i­nitely no cur­rent way to set a perma­link struc­ture for a par­tic­u­lar post_type, and there­fore also no way to set spe­cific tem­plates for those.

  8. Posted April 20, 2010 at 2:35 pm | Permalink

    Some of the endpoint/rewrite stuff is indeed unfin­ished. I’d like to see enhance­ments to end­point sup­port how­ever: http://core.trac.wordpress.org/ticket/12779.

    I’m glad you’ve warned that gen­er­at­ing rewrite rules are expen­sive. They’re best left to an acti­va­tion hook for sure.

  9. Matt
    Posted April 20, 2010 at 2:48 pm | Permalink

    @Nacin It’d def­i­nitely be great if the rewrite stuff was brought into core. I’m afraid that the end­ponit stuff makes my eyes glaze over. :)

  10. Posted April 24, 2010 at 3:22 am | Permalink

    I stum­bled here via google. The rewrite rules for Cus­tom Posts have got me in a frenzy today. Have you looked into how you would add the cat­e­gory (i.e. cus­tom tax­on­omy) into the url?

    (Really impres­sive work, by the way)

  11. Matt
    Posted April 25, 2010 at 11:10 am | Permalink

    @Lane Cus­tom tax­onomies already have good rewrite sup­port by default.

  12. Tom
    Posted April 28, 2010 at 10:40 am | Permalink

    I’ve down­loaded the helper but how do I include it? I have pasted it into the top of the functions.php and then at the bot­tom, below the post_type def­i­n­i­tion I have included the sd_register_post_type( ‘typename’ );

    What do I need to have in the perma­links structure?

    With­out using the helper, leav­ing the perma­link struc­ture on default the web­site works per­fect. But if I change the perma­links to my stan­dard “/%category%/%postname” I get page not found!

    Thanks :)

  13. Matt
    Posted April 28, 2010 at 11:16 pm | Permalink

    @Tom includ­ing it in functions.php the way you are should work fine.

    The perma­link struc­ture for the sin­gle cus­tom post itself is already baked into WP (my class has noth­ing to do with that part). It doesn’t, how­ever, pay atten­tion to your perma­link set­tings — it’ll only pro­duce http://yourdomain.com/posttype/slug-goes-here/ perma­links. This is because there’s no guar­an­tee that your cus­tom post type will even have a cat­e­gory, unlike nor­mal posts. You can prob­a­bly change this by set­ting proper $args which my helper func­tion passes to register_post_type(), but I haven’t looked into it.

  14. Tom
    Posted April 29, 2010 at 4:59 am | Permalink

    Thanks for the reply Matt. With­out the helper the sin­gle post pages work fine http://yourdomain.com/posttype/slug, with any perma­links structure.

    I only run into a prob­lem when I want to go to http://yourdomain.com/posttype/ which would be a list­ing page for all posts within that post type. That works fine with Default perma­link struc­ture but as soon as I change it to a cus­tom one, I get “Not Found” regard­less of includ­ing your helper or not.

    Is this a sep­a­rate prob­lem that needs look­ing into as I’m yet to find a work­ing solution.

  15. 10SexyApples
    Posted April 30, 2010 at 12:46 am | Permalink

    Hi there … and thanks so much for your work on this issue. You are one of only 2 peo­ple that I have been able to find over the last uhm 4 months that has tack­led any­thing to do with the rewrites on cus­tom post types for an archival type tem­plate. That said, I am curi­ous if you would con­sider mak­ing a ver­sion of your helper class that deals only with the perma­link rewrites and tem­plate redirects.

    To explain:
    There are now some very good GUIs for reg­is­ter­ing and man­ag­ing your cus­tom post types. I went from doing it myself in func­tions to using CMS Press, which is very well writ­ten and pro­vides a great inter­face so that you don’t have to have pages and pages of code in your func­tions if your using a lot of … I digress.

    Any­who, using myself as an exam­ple, I’ve already gone two … maybe ten … rounds with reg­is­ter­ing and set­ting up my cus­tom post types. I’ve even made tem­plates that query all my cus­tom post types for lack of being able to find this info pre­vi­ously, and because reg­u­lar expres­sions and rewrite rules make my head spin.

    Your code is beau­ti­fully done and han­dles exactly what is, I think, the miss­ing link in every­one else’s code/explanation/plugin.

    Prob­lem is, it han­dles too much you see? I can’t re-register my post types at this stage in the game, and I’m not sure how to dis­sect your code to remove what’s redun­dant. I don’t want to be “exper­i­men­tal” with it.

    Only other option is to just do my own rewrites from scratch in func­tions, but, I would have to do each post type man­u­ally, and that wouldn’t be any­where near as clever as your code 😉

    What do you say?

  16. Gurjeet Singh
    Posted April 30, 2010 at 10:57 am | Permalink

    Read­ing through this, it seems it will do every­thing that I want it to!!

    Try­ing it though, doesn’t do what I expect. I have included your code and reg­is­tered a post type and all works fine.

    I cre­ated new one called ‘per­sons’ using:

    sd_register_post_type(‘person’);

    which works fine. Sooo sim­ple to do. Thank you! I have a cus­tom perma­link of:

    /%category%/%postname%/

    and added a new post in ‘per­son’ called ‘test’ gives me URL of:

    mywebsite.com/person/test/

    Great, also loads up the cus­tom tem­plate from /person/single.php. How­ever, going to:

    mywebsite.com/person/

    Gives me a 404, should that give me a ‘land­ing’ page call­ing in /person/index.php.

    Am I not under­stand­ing this right?

  17. Tom
    Posted April 30, 2010 at 11:00 am | Permalink

    Thanks for your help Matt, I finally solved what I was try­ing to do and I didn’t have to use your helper in the end.

    Gur­jeet — I was try­ing to do the exact same thing, hav­ing a list­ing page for all posts within the cus­tom post type, and the sin­gle cus­tom post type page.

    Cre­ate the cus­tom post types, then cre­ate your perma­links, then add data. If you start to add data and then change the cus­tom post types the data­base doesnt like it. I had to clear out my data­base and now every­thing works per­fectly with­out the need of the sd_ helper!

  18. Gurjeet Singh
    Posted April 30, 2010 at 11:11 am | Permalink

    Thanks Tom,

    Have got a test install of word­press so I just cleared the database.

    Func­tions file already sets up cus­tom post type of ‘per­son’ so that bit is done. Went in and updated perma­links. Then went and added con­tent into ‘per­son’ cus­tom post type.

    When view­ing, the actual page works (/person/test/) but /person/ still gives 404. Tried updat­ing perma­links again and no joy.

    What steps did you take to get it to work Tom?

    Some more doc­u­men­ta­tion on this would be amaz­ing because it says it will do all I need — If only I could get it to work lol.

  19. Matt
    Posted April 30, 2010 at 1:17 pm | Permalink

    @10SexyApples Just com­ment out or remove this line:

    add_action( 'init', array($this, 'register_post_type'));

  20. Matt
    Posted April 30, 2010 at 1:23 pm | Permalink

    @Gur­jeet I don’t know what Tom’s talk­ing about regard­ing clear­ing the data­base, but I spell out where the cus­tom post land­ing page will occur above. If you use sd_register_post_type("person") (and then go refresh your perma­links as I explain above), your land­ing page for the “per­son” post type will be at http://yourdomain.com/persons/ <– note the plural. I explain what I’m doing with plu­rals above, and I also note that there’s a way to explic­itly over­ride this with a third argument.

  21. 10SexyApples
    Posted April 30, 2010 at 3:06 pm | Permalink

    haha­hah. Thanks so much Tom for the quick response. I feel like an idiot for not know­ing that all I needed to remove was the action. Well, live and learn right?

  22. 10SexyApples
    Posted April 30, 2010 at 5:09 pm | Permalink

    Quick helper for the oth­ers here strug­gling with this.

    After you have included Tom’s helper func­tions, and have added the appro­pri­ate func­tions to reg­is­ter your post types with his helper class –>

    If you intend to have an archive type page just for your cus­tom post types and have cre­ated the $custom_post_type_name.php file, you still need to run a query like:
    •query_posts(‘post_type=$custom_post_type&posts_per_page=-1′)•

    prior to your loop in your $custom_post_type_name.php file to bring in the posts. They won’t just auto­mat­i­cally show up there. You will get a 404 not found with­out this.

    I think this is where a cou­ple peo­ple are get­ting con­fused, so, thought I would men­tion it.

  23. Matt
    Posted April 30, 2010 at 5:45 pm | Permalink

    @10SexyApples First off, my name is Matt, and I wrote the class, not Tom. And sec­ondly, that should all be unnec­es­sary, as the helper class should be doing that all for you. Are you using WP 3.0?

  24. 10SexyApples
    Posted April 30, 2010 at 7:02 pm | Permalink

    Oh my good­ness, I’m so sorry. My face is all red. And yes, I’m using 3.0, but, I think I have just real­ized what is hap­pen­ing, so, sorry for the attempt to be help­ful, I failed mis­er­ably at that.

    I am using twenty ten as a par­ent theme and build­ing my theme as a child of it.

    It seems that the tem­plate that is being pulled for my single-$custom_post_type.php is the 404 tem­plate from the twenty-ten theme. I’m not sure why, but, assum­ing it has some­thing to do with the locate_template func­tion as this was work­ing fine before.

    I am going to assume that that is prob­a­bly the rea­son I was get­ting the 404 on my $custom_post_type.php tem­plate as well, and only got results when I put the query in there.

    Any ideas?

  25. 10SexyApples
    Posted April 30, 2010 at 7:22 pm | Permalink

    My cus­tom post type is “lesson”.

    I removed the cus­tom query from lesson.php and just have the default loop.

    When I go to /lessons
    It redi­rects to:
    wp-content/themes/twentyten/404.php
    with no posts found

    When I go to a sin­gle post of cus­tom post type “les­son” it redi­rects to:
    wp-content/themes/cota/lesson.php
    with just that sin­gle post returned.

    which is where it should be redi­rect­ing for /lessons correct?

    I am using CMS Press to han­dle man­age­ment of my cus­tom post types and have the perma­links there set up as:

    %identifier%/%year%/%postname%-%post_id%.html

    I have the register_post_type action com­mented out in your helper func­tion as instructed.

    And my nor­mal perma­link set­tings are:
    /%year%/%postname%-%post_id%.html

    Could this get any more con­fus­ing? I’m com­pletely lost now. Does this make any sense to you? If it does, please clue me in as I’m really start­ing to pull my hair out 😉

  26. Matt
    Posted April 30, 2010 at 10:33 pm | Permalink

    @10SexyApples Sorry, but I have no idea how CMS Press is reg­is­ter­ing its cus­tom post types, and I can’t speak to how they’d inter­op­er­ate. There’s obvi­ously some kind of incom­pat­i­bil­ity between their method­ol­ogy and mine.

  27. 10SexyApples
    Posted April 30, 2010 at 10:52 pm | Permalink

    I see. That’s actu­ally what I should have been more clear about in my first com­ment. CMS Press is only han­dling the perma­link for the sin­gle posts from what I understand.

    Can you tell me what to com­ment out to remove the helper func­tion from rewrit­ing any­thing to do with the sin­gle post? Per­haps that would take care of this.

    I have just fin­ished remov­ing my theme from being a child to see if that has any effect, as it was only when I added the helper func­tion that any­thing began not mak­ing it past the par­ent theme.

    I’ll report back in a sec on that.

  28. 10SexyApples
    Posted April 30, 2010 at 10:57 pm | Permalink

    Hang on a sec, you real­ize that I am reg­is­ter­ing the cus­tom post types via CMS Press, and not using the register_post_type action from the helper func­tion right?

    These post types were reg­is­tered long ago and work­ing per­fectly with the “fake” archive and WP default single-lesson.php templates.

    I’m just try­ing to get a proper “archive” page of cus­tom post types, it’s the only thing that is miss­ing, so, from what I under­stand I just need a rewrite of ?post_type= and a redi­rect to /lessons of any query var that has the post type, but, not the sin­gle name.

    Man, I sure wish that dis­cus­sion in trac about includ­ing this in core had taken a dif­fer­ent turn. I really think leav­ing this out is going to open a can of worms for every­one who gets near it.

    Great to learn from though 😉

  29. 10SexyApples
    Posted April 30, 2010 at 11:12 pm | Permalink

    Elim­i­nat­ing the child theme cleared up the redi­rect going to the wrong theme file 404, but, noth­ing else.

    On to the next test

  30. 10SexyApples
    Posted April 30, 2010 at 11:16 pm | Permalink

    I wrote all of my cus­tom post type args myself, and then when I decided to use CMS Press for it’s GUI et al, I actu­ally con­structed the args in that code to match the way that I wanted them to be, as well as updated them, as they were not quite up to par with what was cur­rent in trac for options in the sup­port array.

    The only thing I don’t under­stand is the rewrite stuff.

    So, if you think it might help myself or any­one else to get this cleared up, ask away please. I’ll look up and test what­ever is nec­es­sary to get this solved~

  31. 10SexyApples
    Posted May 1, 2010 at 1:23 am | Permalink

    Okay, at the risk of look­ing like a com­plete maniac being the only one talk­ing here, I will con­tinue to doc­u­ment what I find here in the hopes that it will save some­one else from the hours/days … months now work­ing all of this out.

    Lat­est devel­op­ment is that the guy that wrote CMS Press is also one of the main devel­op­ers work­ing on all of the cus­tom post type func­tions in the core. He has a diff up on the trac right now to have WP first look for $post_type.php to see if it exists before default­ing to single-$post_type.php. It hasn’t been merged yet, so, he has writ­ten it tem­porar­ily into CMS Press.

    So, if you are using CMS Press and attempt­ing to use this helper func­tion, your sin­gle posts are going to try to access the same tem­plate as your archive.

    I would sug­gest leav­ing the func­tion­al­ity of hav­ing $post_type.php for sin­gles as it will prob­a­bly end up core behavior.

    Matt, can we get your helper func­tion to look for $post_types.php as the tem­plate, just like the plural on the url, instead?

  32. 10SexyApples
    Posted May 1, 2010 at 1:41 am | Permalink

    Okay, finally, so chang­ing the helper func­tion to look for a post_types.php instead of a post_type.php tem­plate worked beau­ti­fully, and this is the last you’ll hear from me. Hope­fully my ram­blings will come in handy for some­one. I now have work­ing sin­gle cus­tom post type pages as well as a cus­tom post type archive.

    Thanks Tom! … Just kid­ding, Thanks Matt!!!!

  33. Gurjeet Singh
    Posted May 5, 2010 at 5:10 am | Permalink

    Thanks all for your help and Thanks Matt for this great bit of code that sorts this stuff out.

    When read­ing through your post, I didn’t exactly get what the whole plural bit was on about but I do now :) BTW, how can this be switched around so it doesn’t do plural but you can over­ride to make it plural?

    Few sug­ges­tions though please:

    1) Would you be able to do an exam­ple of an ‘sd_register_post_type’ pass­ing in args array to see how its sup­posed to be done?

    2) You have done a great job doing this — if you could write some­thing that could make it just as easy/easier to include cus­tom meta boxes (First name, last name etc for per­sons — movie name, year for movies etc) then that would be AMAZING! Would defo be like a full cus­tom CMS sys­tem then!!!

    Thanks in advance!

  34. Gurjeet Singh
    Posted May 6, 2010 at 6:28 am | Permalink

    Actu­ally, just a heads up… some­one has already writ­ten a Cus­tom Meta Type sys­tem over at: http://www.deluxeblogtips.com/2010/05/howto-meta-box-wordpress.html

    I’m using this and that together and I now have the abil­ity to make Word­Press my very own cus­tom CMS.

    Thanks!

  35. Posted May 6, 2010 at 6:48 am | Permalink

    Very nice approach. I love the idea of tem­plate struc­ture. It’s great for web­sites that have many post types.

    Your code need to be look very care­fully to under­stand :). I like it. Thanks for sharing.

  36. dave
    Posted May 11, 2010 at 8:02 pm | Permalink

    I’m a lit­tle stuck on this one.

    I started by copy­ing all the included code into my theme functions.php file.

    Below that I add the following:

    add_action('init', 'my_custom_init1');
    function my_custom_init1() {
        $args_array = array(
            'label' => __('Albums'),
            'singular_label' => __('Album'),
            'public' => true,
            'show_ui' => true,
            'capability_type' => 'page',
            'hierarchical' => false,
            'rewrite' => true,
            'supports' => array('title', 'thumbnail')
            );
    
        sd_register_post_type( 'albums' , $args_array );
    }

    Prob­lem is I am not get­ting albums to show up in my admin inter­face. If I get rid of the sd_ then it works.

    Must be doing some­thing really stu­pid here.

  37. Matt
    Posted May 11, 2010 at 10:24 pm | Permalink

    @dave The rea­son it’s fail­ing is pretty sim­ple: my class already does all the action hooks for you. Since you’ve called sd_register_post_type() within a func­tion that gets called at init, it’s too late for the helper class to reg­is­ter its own init code.

    Solu­tion: take the sd_register_post_type() out of my_custom_init1() and put it inline. Or, if you’re attached to the idea of hav­ing your own init hook, make sure your action gets a very high pri­or­ity like this: add_action('init', 'my_custom_init1', 0);. An action with­out a num­ber gets called at pri­or­ity 10 by default, so 0 would make sure it ran early enough to get all of my actions hooked up.

  38. angga
    Posted May 14, 2010 at 2:39 am | Permalink

    hi matt, great class.

    btw, is pos­si­ble to make post_type archive

    eg: wp/post_type/2010/05/14/my_post

    how to make the cor­rect rule 😛

  39. Simon B
    Posted May 16, 2010 at 9:37 am | Permalink

    Hi

    I see on a recent post on the wp-hackers list ( http://groups.google.com/group/wp-hackers/browse_thread/thread/60eba1cd65758237# ) that the way labels and capa­bil­i­ties are han­dled by register_post_type is changing.

    Will you be updat­ing your helper class to reflect this?

  40. Matt
    Posted May 18, 2010 at 4:44 pm | Permalink

    @Simon Yeah, it’ll be edited. The ben­e­fit here is that I’m using it myself, so it gets my attention.

  41. Matt
    Posted May 18, 2010 at 4:46 pm | Permalink

    @angga I’m sure it’d be pos­si­ble, but it’d require more regex-fu than I have.

  42. Matt
    Posted May 19, 2010 at 12:20 am | Permalink

    NOTE: I’ve moved this into its own project page, where you can now find the down­load link and leave com­ments. This post will no longer be updated and com­ments are now off.