Revisiting Custom Post Types, Custom Taxonomies, and Permalinks

Published 2 years 10 months ago on May 20, 2011 — 8 min read

This article is an extension of both Custom Post Types, Custom Taxonomies, and Permalinks in WordPress 3.0 and WordPress Archive Pages Based on Custom Taxonomy as WordPress has solved a number of the issues covered in those publications.

I’m using Custom Post Types more and more, especially given the Pods 2.0 roadmap. I’ve got an entire philosophy behind how I dictate what should be a Custom Post Type, a Custom Taxonomy, or a Pod, but we’ll leave that for another day. I’d like to talk about how to best architect your Custom Post Types with a focus on integrating your Custom Taxonomies to the best of your ability.

As should be a focus for us, I have a strong principle as I’m building out a site to make sure the URL structure makes sense. This isn’t for SEO, online marketing, buzzwords, or social media. This is for the real life people reading these URLs. I’m a big proponent of retaining a folder structure, and as per the now outdated publication first covering this topic, we can make things even better.

Setting up your Custom Post Type

If you’re still using the Custom Post Type UI that’s all well and good, but I would suggest eventually moving away from that and hand coding your Custom Post Types just to raise your own bar of knowledge over time.

The first thing I always do when structuring my Custom Post Type and Custom Taxonomy relationships is to have a plan. You’ll see why in a minute, but it’s really important to have a URL structure game plan before writing any code or setting anything up. I’m going to use the same example as last time and focus on the situation of wanting to have a site that focuses on cameras and camera gear. That said, I’m going to want the following:

  1. A Custom Post Type named cameras
  2. A Custom Taxonomy named brands

Note: I’ve got a policy to prefix my Custom Post Types/Taxonomies as a way of preventing undesired behavior should a client some day switch themes or something to that effect. For the sake of this example I’ll leave the Custom Post Type/Taxonomy names as is for readability.

Given this relationship, I’d like my URL structure to resemble the following: My Cameras section should find a home at http://example.com/cameras/, when viewing a single camera I’d like the URL to piggyback the ‘base’ URL and append the camera slug: http://example.com/cameras/canon-7d/.

Since the brands Taxonomy is directly related to the cameras, I’d like to also piggyback that same ‘base’ URL as we’re able to out of the box with a Custom Post Type, something like http://example.com/cameras/brands/. This can be a bit of a tricky achievement if not implemented properly. Even when implemented properly, there are a couple of things you’ll want to keep a close eye on should you go with this type of implementation.

The take-home message here is the fact that in order to piggyback the ‘base’ URL as outlined above, we need to define the Taxonomy first. It all comes down to knowing WP_Rewrite and making it your friend.

Setting up your Custom Taxonomy

Our next step (after determining our URL structure) will be to add our Custom Taxonomy:

$labels = array(
    'name'                          => 'Brands',
    'singular_name'                 => 'Brand',
    'search_items'                  => 'Search Brands',
    'popular_items'                 => 'Popular Brands',
    'all_items'                     => 'All Brands',
    'parent_item'                   => 'Parent Brand',
    'edit_item'                     => 'Edit Brand',
    'update_item'                   => 'Update Brand',
    'add_new_item'                  => 'Add New Brand',
    'new_item_name'                 => 'New Brand',
    'separate_items_with_commas'    => 'Separate Brands with commas',
    'add_or_remove_items'           => 'Add or remove Brands',
    'choose_from_most_used'         => 'Choose from most used Brands'
    );

$args = array(
    'label'                         => 'Brands',
    'labels'                        => $labels,
    'public'                        => true,
    'hierarchical'                  => true,
    'show_ui'                       => true,
    'show_in_nav_menus'             => true,
    'args'                          => array( 'orderby' => 'term_order' ),
    'rewrite'                       => array( 'slug' => 'cameras/brands', 'with_front' => false ),
    'query_var'                     => true
);

register_taxonomy( 'brands', 'cameras', $args );

The above snippet can be tossed in your theme’s functions.php and will set up our Custom Taxonomy. If we were to visit the WP Admin at this stage, we wouldn’t see any change because we haven’t created cameras yet. Take extra note of the rewrite key in our $args array. That value is telling WordPress the slug to use in our Custom Taxonomy. We don’t want it prepended with the WordPress permalink front base, which defaults to true, and we want the slug to include our Custom Post Type rewrite slug.

With our Custom Taxonomy created, we can continue on to implementing our cameras Custom Post Type.

Integrating our Custom Post Type with our Custom Taxonomy

It’s very important that your Custom Post Type snippet is placed after your Custom Taxonomy snippet in functions.php. That ties in with how WP_Rewrite works as the process of deciphering permalinks is, in a way, top-down. Our Custom Post Type will be added like so:

register_post_type( 'cameras',
    array(
        'labels'                => array(
            'name'              => __( 'Cameras' ),
            'singular_name'     => __( 'Camera' )
            ),
        'public'                => true,
        'show_ui'               => true,
        'show_in_menu'          => true,
        'supports'              => array( 'title', 'editor', 'thumbnail' ),
        'rewrite'               => array( 'slug' => 'cameras', 'with_front' => false ),
        'has_archive'           => true
    )
);

Again, it’s important that this snippet be inserted after your Custom Taxonomy snippet that wishes to piggyback the Custom Post Type slug. After saving functions.php with both snippets, your Custom Post Type and Taxonomy will be ready to use:

The next big part will be making sure your permalinks are working. One of the greatest things to be included in core since the last writing, is the has_archive key in our register_post_type() arguments. This optional argument unlocks the ability to have a traditional archive view of your Custom Post Type without needing a custom Template.

Note: Something very often forgot by new(er) Theme developers is the fact that you must visit your Settings > Permalinks options screen in the WordPress admin to ensure your new permalinks are picked up. You don’t need to edit anything, just view the page.

Template files you’ll need

To get up and running we’ll first begin with our Custom Post Type archive. While referred to as an ‘archive’ it almost seems better to refer to it as an index semantically as it’s not really an archive in the traditional sense. Nomenclature aside, we’re going to follow the WordPress Template Hierarchy and create archive-cameras.php (archive-{CPT_NAME}.php) to which WordPress will toss The Loop for our cameras Custom Post Type. We can literally copy the contents of index.php there for the time being, to be customized later.

<?php get_header(); ?>

<div id="container">
    <div id="content" role="main">
        <h1>Cameras</h1>
        <?php get_template_part( 'loop', 'index' ); ?>
    </div><!-- #content -->
</div><!-- #container -->

<?php get_sidebar(); ?>
<?php get_footer(); ?>

When that file has been created, visiting http://example.com/cameras/ will hit that template file and run through The Loop.

If you’d like to have a custom number of posts per page, you can do that too.

Again, following WordPress Template Hierarchy, clicking through to the Custom Post Type single page will use your single.php Template file, unless you create single-cameras.php. In that case, WordPress will use your CPT-specific Template file.

Now that we’ve implemented browsing Custom Post Types in our Theme, we need to include the appropriate Custom Taxonomy Template files as well. Keep in mind that this only works because of the 'has_archive' => true argument used when setting up the Custom Taxonomy. The beauty here lies in the fact that we can now create a taxonomy-brands.php Template file which will be used when browsing our Custom Taxonomy, in this case camera brands. As we did before, we can copy the lowest common denominator index.php to get started with our Custom Taxonomy archive Template file:

<?php get_header(); ?>

<div id="container">
    <div id="content" role="main">
        <h1>Brand Archive</h1>
        <?php get_template_part( 'loop', 'index' ); ?>
    </div><!-- #content -->
</div><!-- #container -->

<?php get_sidebar(); ?>
<?php get_footer(); ?>

When taxonomy-brands.php is saved, we can now visit http://example.com/cameras/brands/canon/ to view our Custom Taxonomy archive:

Clicking through will again bring us to the applicable single Template file.

There is one ‘gotcha’

Although our permalinks are working in the way we originally intended, there is an important fact to take into consideration. There is a use case, albeit unlikely, that would prevent the viewing of a Custom Post Type entry. Take for example, the possibility that we had a camera called “Brands”. With the way we’ve set up our Custom Taxonomy, that slug would never be viewed because of the top-down way our permalinks are handled. To be more generalized, if there’s potential for a Custom Post Type slug to match that of your Custom Taxonomy, use a different implementation and sacrifice your ideal URLs. This circumstance is unlikely, but it’s a drawback you should be aware of should you go after an implementation such as this.

There's a conversation brewing

  1. Nice breakdown, although I would put the CPT logic in a plugin instead of the functions.php of your theme to achieve higher flexibility.
    Ryan Imel explains it pretty well here: http://wpcandy.com/teaches/how-to-create-a-functionality-plugin

  2. I have done this before, but have been working at using the has_archive feature in custom post types and implementing the singular word for the view all archive page and plural for the permalink structure. Though this creates a small issue when you need to add a redirect for the plural page if you remove the taxonomy/post title in the url.

  3. I would be very interested to learn about the philosophy behind how you dictate what should be a Custom Post Type, a Custom Taxonomy, or a Pod. So many different ways to go with these…

  4. Jonathan,

    As you mentioned in the last paragraph of this article, the permalink will 404 if the slug contains any of the same words used as the taxonomy.

    This is actually a big bummer for me right now on one of my projects.

    I’m really getting sick and tired of 404′s with WordPress. The whole Permalink deal seems so fragile.

    Do you think future WordPress versions will be able to get around such a thing? Or even, a plugin?

    • I can’t really say for sure. I’m not even sure that my example is intended usage by the WordPress core contributors. With Custom Post Types and Custom Taxonomies being so new, I do imagine it’s an active arena for thought, and I’ll definitely be keeping my eye on it!

  5. I am just getting my feet wet with this whole custom page/custom taxonomy/permalink business and came across this post. It has helped a ton. Since I have no experience, I am interested in your philosophy: “I’ve got an entire philosophy behind how I dictate what should be a Custom Post Type, a Custom Taxonomy, or a Pod, but we’ll leave that for another day. ”

    I have an example, if it would help you to explain your philosophy…How about a wordpress site that simply displays quotes and quote authors. For example, a post would look like this:
    “Sometimes I go left, sometimes I go right.” -Steve Stevenson.

    Ideally, the permalink structure to all quotes by Steve Stevenson would be something like: example.com/steve-stevenson or example.com/quotesby/steve-stevenson.

    How would you apply your philosophy to that gem? Thanks!

  6. cool – got this working although the taxonomy-brands page doesn’t show the header ‘brand archive’ for some reason.

    But how would you display your custom post tag posts on the homepage index.php?
    I’m trying to duplicate a tumblr style system with masonry.

  7. So your taxonomy lives here:

    http://example.com/cameras/brands/canon/

    Is there a way (other than building it all manually) to have

    http://example.com/cameras/brands/

    Display all the brands?

  8. Wow – this has been an invaluable post. I stumbled upon this when searching for a solution to my custom taxonomies not correctly paginating (ie. the custom-slug/taxonomy-slug/page/2 arrangement was giving a 404).

    Reversing the declaration of the custom taxonomy and custom post type remedied the problem pronto. I’m astonished that this is the *first* I’ve come across a mention of this requirement, despite extensive, agonizing, head-beating-against-desk struggles to get custom post types and taxonomies working properly… AWESOME information. The damned codex should link here.

  9. Thanks for writing this article, I’ve looked at a lot of other resources regarding custom post types and none of them come close to being as clearly written as this.

    Thanks!

  10. Thank you! The tip about registering the post types AFTER the taxonomies was super helpful. I had mine the other way around and could never get the post type slug in the url because of it. Just switched them around and everything now works perfectly.

  11. Hi there. Your article is quite useful, and is almost what I’ve been looking for in a while. However, I have a question not dissimilar to what others have asked.

    The structure I have in mind is as follows:
    http://www.example.com/cameras/ – a custom page (e.g. news about cameras)
    http://www.example.com/cameras/canon – a list of all cameras by Canon
    http://www.example.com/cameras/canon/7D – a page about that specific camera.

    My actual site is about bands and songs, but the brand/product metaphor also seems to apply. Would you advise me on what to do in this case?

  12. I am kind of in the same boat as all these folks but mine goes a little deeper where as I have multiple CPT’s that are relying on on some centralized taxonomy / terms to break up the various sections of the site but still be able to relate all the content to each other.
    Custom post types of press releases, published articles, expert articles, jobs, etc. These are in turn all broken up by the main taxonomy of markets and it’s various terms of market 1, market 2, market 3 etc.

    My issue is since all these different CPT’s are sharing the same taxonomies I need a clean way of separating them all out. Right now I am relying on the URL structure of /market/food/?post_type=press-releases where I would rather be using press-releases/food or worst case press-releases/market/food. Being able to do the same with any of my other CPT’s would be ideal. So jobs would be jobs/food or whatever.

    c.

    • @Carlitos – I have *exactly* the same issue. Have you found a solution?

      @Jonathan – Thank you for this article. To extend your example, what if the site you’re building was for electronics, rather than just cameras? We might have other CPTs, like “Printers”, that share the same Brand taxonomy. So the taxonomy term “Canon” would be used by both Cameras and Printers, and we’d try to build urls like:

      - /cameras/brands/canon/ — shows Canon cameras
      - /cameras/brands/nikon/ — shows Nikon camera
      - /printers/brands/canon/ — shows Canon printers
      - /printers/brands/brother/ — shows Brother printers
      [ / CPT / Taxonomy / Term ]

      Like Carlitos, I wouldn’t mind if “brands/” (the Taxonomy name) were omitted, but it’s fine either way.

      thanks, -todd

  13. Great post! How would I create a single.php that will only be used by my custom post type camera, where brand = ‘nikon’ ? I’d like to use a separate single.php when the brand = ‘leica’.. Hope that makes sense!

  14. Thanks for this post, the “register taxonomy BEFORE post type” solved my issue … frightening to think how much time I would have spent troubleshooting had you not clarified that! Thanks! -Joe

  15. Thanks so much for this post! It has helped me out tremendously – I am still looking for a solution to the problem of not being able to have anything at http://example.com/cameras/brands – has anyone come up with a solution?

  16. Best tutorial on this topic, thank you for taking the time to share this…you saved my butt man!

  17. For where you want

    /cameras/brand/cannon/b7

    You could include this in the permalink structure

    /%year%/%monthnum%/%taxonomy%/%postname%/

    then /cameras/brand/cannon/b7 will not error it will however 301 redirect to

    /cameras/b7

    only way i have found to do it atm

  18. Thanks for this post – your solution for piggybacking a taxonomy slug on a CPT slug is just what I wanted.

  19. What about for a tax. thats applicable to mulitple CPTs, such as the default “categories”.

    I’d like
    /cameras to point to all cameras
    /cameras/portable to point to all cameras in the portable category
    /videocameras/portable to point to all video cameras in the (same) portable category.

    Is this possible?

  20. I have had something very similar set up and functioning well for a while. I’ve recently been overhauling my theme and just about done with everything but I just realized that my custom taxonomy pagination was broken. I was getting the dreaded 404 on page 2. I wasn’t specifying posts_per_page which seemed to be the cause for most people so I ended up spending hours tracking it down. After I read your post and tried it in a clean install of WordPress–and pagination worked–I was able to track down the issue.

    I noticed that I had set some additional arguments in my custom post type arguments array that you had not specified. All of mine I got from the WP Codex, so I would assume they are legit. I decided to remove them one at a time to see if the issue cleared up–and it did!

    When I removed ‘exclude_from_search’ =>true from the arguments, the pagination problem went away!

    As defined in the codex:
    exclude_from_search
    (boolean) (importance) Whether to exclude posts with this post type from search results.
    Default: value of the opposite of the public argument

    I don’t really know why this broke it, but it absolutely did. I don’t really want these posts showing up in search results, but I can solve that a different way.

    So.. anyone with the dreaded 404 on custom taxonomy pagination, try removing ‘exclude_from_search’ from your CPT arguments.

  21. in my post type, I have ‘rewrite’ => array( ‘slug’ => “work/$name”, ‘with_front’ => false ),

    and the archive page loads fine at site.com/work/design

    but when I load a post of type design, the permalink reads site.com/design/post-title/

    I need to put work there… any ideas?

    • solved that one, now I’m trying to do the same with my taxonomy.

      ‘slug’ => “$post_type” works

      but

      ‘slug’ => “work/$post_type” fails…

      thoughts?

  22. Hey Jonathan,

    I’ve worked on a myriad of projects this past year where I found this post immensely valuable. So, thank you.

    However, I’ve still run into the same problem over and over: getting the URL structure to reflect custom taxonomies in custom post types.

    I came across this article today while building a small site. It seems to add on to what you’ve explained in this post and your previous post.

    http://wordpress.stackexchange.com/questions/39500/how-to-create-a-permalink-structure-with-custom-taxonomies-and-custom-post-types/39862

    Let me know your thoughts. I always take the stance that I’m just missing something, but it seems I might not be the only one having this trouble with WordPress’s capabilities. I suppose this is where Pods comes into play, though. :-)

  23. I got everything working but the final taxonomy page shows no posts in that category. I’m 100% sure I have several posts under the category. Any idea what that would be?

  24. Super helpful article.

    Cheers.

  25. Thanks for the article… working for me.. :)

By all means, contribute

Leave a comment

Powered by Fusion

This article is so meta

Published May 20th, 2011

Random article

css.php