It’s taken me some time (and a lot of fiddling!) to get to a stage where I am comfortable with being able to create custom post types for a variety of scenarios, giving me more confidence using WordPress as a CMS.
Giveaway
Say hello to a WordPress framework built for CMS purposes with two example custom post types; one hierarchical (Products) and one with custom taxonomies (Recipes). The theme is focused on solving two common problems — achieving a template & menu architecture that works solidly, with permalinks that contain a taxonomy term in the URL e.g. /post-type/taxonomy-term/post-name
Products are essentially a replica of the default Pages functionality, and Recipes are a replica of Posts. The aim is to prove WordPress can be extended with flexible post types and search-friendly permalinks that mirror what comes out of the box.
Caveat: the CSS/styling is a bit primitive, the focus here is on WordPress!
About
This framework is intended as an extension of existing themes, giving you the templates and functions needed to get you up and running with your own Custom Post Types. As such this theme does not contain some of the standard WordPress templates (e.g. the blog), but it could be used as a starting point for a more straight-up CMS project. You may also find parts useful in other areas of your own theme, so take what you need!
Installation
- 1.) Create a fresh install of WordPress on your server
- 2.) Download the theme and upload to the
/wp-content/themes
folder - 3.) Activate your theme via the Appearance menu
- 4.) Set your permalink structure to
/%category%/%postname%/
- 5.) You’re done. Begin adding your Products and Recipes!
Notes
Here is a breakdown of some of the functions and architecture used in the theme. If you’re unsure of anything or need help, comment below or get in touch via Twitter. Enjoy!
Products
Achieved by a hierarchical post type — product
— defined in functions.php
. If you are unsure about the majority of the code for Custom Post Types, I suggest you read the WordPress Codex.
Line 287
defines the slug
for the Products section and the base for any posts within it:
'rewrite' => array( 'slug' => 'products', 'with_front' => false )
We do away with the traditional archive-product.php
template and create our own page called Products. This page should take the same URL/slug as defined in our post type. I have found this is a much more stable method of producing a post type index page.
These two parts combined give you the flexibility of setting the base slug anywhere on your site (even 5 or 6 folders deep), then using a page to display your post type index without conflict. Custom Post Types need not always sit at root level!
We assign the page template products.php
to act as our section index. A custom function — get_root_pages('product')
— is used here to query all top-level pages for that post type and display them in a custom loop.
As Products are hierarchical, we can assign them parent and order as normal. Using the above rewrite with pretty permalinks enabled, sub-products automatically receive the correct natural URL e.g.
/products/baking
/products/baking/tala-cooks-measure/
Two important things in order to create a hierarchical post type are 'hierarchical' => true
and 'supports' => array('title', 'page-attributes')
during the register_post_type
function.
register_post_type( 'product',
array(
'capability_type' => 'page',
'hierarchical' => true,
'supports' => array('title', 'page-attributes', 'editor', 'excerpt', 'thumbnail'),
)
);
single-product.php
works as standard to display products, but contains a custom function — get_sub_pages($post->ID, 'product')
— to check for child pages and display them in a custom loop at the bottom of the page if found.
In-Section Navigation
Sidebar navigation is taken care of in the using a custom function I have written to create in-section navigation for any post type. WordPress’ wp_list_pages
function is okay, but merely lists all pages and adds lots of ridiculous classes to the markup.
<?php insection_subnav('current', 'product'); ?>
This is much more useful for typical navigation — showing sub-pages only for the page or section you’re in — up to three levels deep.
The 'current'
attribute allows you to set the name of the active class on the parent <li>
element of the current page. You could even re-use this function for Pages, just change 'product'
to any given post type.
That’s it. A simple post type allowing you to drill-down via section indexing. If you have the need for something similar to Pages but with lots of custom fields you didn’t want to show within the Pages section, then you’re set!
Recipes
Mimic the default Posts functionality with a creation of another post type — recipe
— in functions.php
. Recipes are non-hierarchical and date sensitive. Two custom taxonomies are also included, recipe_ingredient
(like Tags) and recipe_cuisine
(like Categories).
Again we ditch archive-recipe.php
and use a page called Recipes with its own template — recipies.php
— to show the primary loop/index page for this post type. This also includes pagination exactly as you would expect within a normal blog. Once more, this page can go anywhere and should take the same base slug as our post type.
Taxonomy Permalinks
URLs with custom taxonomies can often be troublesome, including getting a taxonomy term in the URL for a single post. Taxonomy archives also fall under the same logic as normal categories and tags i.e. they need a taxonomy base, otherwise we get a permalink conflict and a 404.
Here, the following URLs are achieved for single-recipe.php
, taxonomy-recipe_cuisine.php
and taxonomy-recipe_ingredient.php
respectively:
/recipes/%recipe_cuisine%/fishermans-curry/
/recipes/type/indian/
/recipes/with/chilli/
Line 411
and line 443
set our rewrites according to where the Recipes section lies, and provides a base for each taxonomy.
'rewrite' => array( 'slug' => 'recipes/type', 'with_front' => false )
'rewrite' => array( 'slug' => 'recipes/with', 'with_front' => false )
It’s not ideal being stuck with a taxonomy base in the URL but I’m not aware of any other method without WordPress throwing a 404. Answers on a postcard!
If you are using taxonomy archive pages hopefully you can come up with something suitable. I often use /filter/
which creates a URL that reads logically i.e. /post-type/filter/taxonomy-name
Aside from that, the rest of the Recipes post type works as-per the Posts section. There are custom wp_list_categories
, get_the_term_list
and wp_get_object_terms
functions called to display taxonomy terms in the sidebar, the loop and on single-recipe.php
Post Permalinks
Line 477
contains the rewrite for our single Recipe page, including the slug of where the Recipes section lies:
'rewrite' => array( 'slug' => 'recipes/%recipe_cuisine%', 'with_front' => false )
You might notice %recipe_cuisine%
looks similar to a tag you might use when setting your permalink structure. This placeholder doesn’t create the URL automatically without another piece of code used to add the taxonomy term:
add_filter('post_type_link', 'recipe_permalink_filter_function', 1, 3);
function recipe_permalink_filter_function( $post_link, $id = 0, $leavename = FALSE ) {
if ( strpos('%recipe_cuisine%', $post_link) === 'FALSE' ) {
return $post_link;
}
$post = get_post($id);
if ( !is_object($post) || $post->post_type != 'recipe' ) {
return $post_link;
}
// this calls the term to be added to the URL
$terms = wp_get_object_terms($post->ID, 'recipe_cuisine');
if ( !$terms ) {
return str_replace('recipes/%recipe_cuisine%/', '', $post_link);
}
return str_replace('%recipe_cuisine%', $terms[0]->slug, $post_link);
}
Note: the function name i.e. recipe_permalink_filter_function
must begin with the name of your custom post type. Everything else should be self explanatory — using the placeholder %recipe_cuisine%
, taxonomy recipe_cuisine
, post type recipe
, and the section slug — within the function.
Other Permalink Stuff
Also included below the Recipe post type is a set of verbose rewrite rules hunted from the depths. This should solve any permalink issues you have, namely fixing a bug where paged archives e.g. /page/2
of any custom taxonomy give a 404 error.
Important: flush_rewrite_rules()
should be added to the end of your last custom post type’s _init
function. I tend to leave this in during development, even though you really only need to run it once. Be sure to remove it when you launch your site or during development when everything is working nicely, it’s very memory intensive!
Other functions
Documented below are some other helper functions which are used in the theme:
<?php my_the_post_thumbnail($post->ID, 1, 'image-size'); ?>
Similarly to the the_post_thumbnail
displays the post’s featured image/thumbnail, but with cleaned up <img>
attributes. Use 1
or 0
to toggle whether the image will be linked to the page it represents (linked by default).
Use the 'image-size'
attribute to define the image variation you wish to display, from any set via add_image_size
(falls back to the default thumbnail size).
<?php link_by_id(15); ?>
Displays a linked title of any post by ID e.g. <a href="http://builtbyboon.com/post-15-link">Post 15 title</a>
<?php $parent_id = get_root_parent_id(15); ?>
Returns the root parent (top ancestor) of any page by ID, useful for active states in primary navigation.
<?php is_recipes(); ?>
A conditional statement which checks if we are anywhere in the Recipes section of the site, including its taxonomy archives. There is also a statement for is_products()
and you might roll your own for is_blog()
. Once more, these conditional statements help for determining active states in primary navigation.
Note: When using Custom Post Types as a primary section, you really need to code your own navigation (at least for your CPT items) as opposed to using something like wp_nav_menu
.
Further extensions
Lastly, here are some neat plugins I recommend to give additional functionality or make your life easier during WordPress projects:
- Advanced Custom Fields -
Easily add custom meta boxes / fields to your post types - Gravity Forms -
The best form plugin there is. Period - Breadcrumb Nav XT -
Create breadcrumb navigation with custom post type support - Remove Title Attributes -
Remove all the annoying View all posts filed undertitle
attributes - WP Page Numbers -
Turn your next and previous links into pagination - Regenerate Thumbnails -
Re-process existingwp_post_thumbnails
when you create a new size - Disable Revisions and Disable Autosave -
Ensure you don’t clog up your database during development