There are a number of nuances to registering WordPress Custom Post Types (CPTs). Here is a list I find helpful when creating CPTs.
- CPTs should NOT be defined in the theme
- Prefix your post type to ensure uniqueness (e.g.
fe_recipe
, not recipe) and use a maximum of 20 characters - Make your post type singular (e.g.
fe_recipe
, not fe_recipes) - Use a rewrite slug
- Make Your Rewrite Slug Plural (e.g.
recipes
) - Add Support for the Block Based Editor (Gutenberg)
- Prepare Your CPT for translation
- Set the rewrite
with_front
value tofalse
- Include a Menu Icon
I’ve included an example CPT definition at the end of this article.
This post was written as a part of my presentation at WordCamp Lehigh Valley 2016, Best Practices for Creating Custom Post Types. The slides that accompanied that talk can be found at ironco.de/presentations/best-practices-for-wordpress-cpt/.
CPTs Should NOT be Defined in the Theme
Defining your Custom Post Type (CPT) in your theme functions.php
works great until you switch themes. Therefore we want to define them somewhere independent from the theme. Where? The mu-plugins
directory.
The mu-plugins directory
The wp-content/mu-plugins/
directory is a special directory that does NOT exist on your site by default (so don’t worry if you don’t have one). If you don’t already have a mu-plugins
directory, you can create one at wp-content/mu-plugins/
.
Once you create the mu-plugins
directory, any file you put it in will be automatically executed. It is like a plugin that can not be turned off.
You can read more about mu-plugins in the codex.
Prefix Your Post Type
When registering your custom post type (CPT), the first parameter is the post type, which is the unique identifier for your CPT. If this unique identifier is the same as someone else’s unique identifier, then they aren’t unique and you have a problem. To reduce the chance of a collision (having the same unique identifier as someone else’s code), use a prefix based on your client name (or your own company name depending on how the plugin will be used).
The post type value can be a maximum of 20 characters, so choose wisely.
register_post_type( 'fe_recipe', $args );
Make Your Post Type Singular
The built-in WordPress Post Types (post, page, attachment, revision, and nav_menu_item) are all singular. Will plural CPT post types work? yes. Should you go back and change existing plugins to singular? no. Moving forward should you make your post type singular? yes.
Use a Rewrite Slug
When we prefixed our post type above, I’m certain some of you cried out, “but now we’ve mucked up our permalinks.” I agree that we don’t want permalinks like http://example.com/fe_recipe/guacamole/. We can accomplish a pretty permalink by decoupling the post type and the permalink. Regardless of our post type, we can independently set our rewrite slug to any value we choose. In this case, recipes
seems more appropriate in the permalink than fe_recipe.
http://example.com/recipes/guacamole/
'rewrite'=> array(
'slug' => 'recipes',
'with_front' => false,
),
At this point, you may be concerned that two CPTs might have Permalink slugs that collide – this is a valid concern. The good news is resolving a collision of rewrite slugs is far easier than resolving a collision of two post type values.
I’ve written more about post types and rewrite slugs in Prefix WordPress Custom Post Type Names.
Make Your Rewrite Slug Plural
Generally in web development, directories are named with plural names (/images/
, /uploads/
, /scripts/
). Since the rewrite slug appears as a directory in your permalink, it is appropriate to use the plural form. I’ve written more about this in Singular vs Plural WordPress Custom Post Type Permalink.
Add Support for the Block Based Editor (Gutenberg)
When WordPress 5.0 was released on 2018-12-06, it introduced a new block based editor (a.k.a. Gutenberg). All WordPress posts and pages will use this new editor, however CPTs will not use the new editor by default.
Because the new editor makes use of the WordPress REST API to read and write information, and CPTs are not accessible via the REST API by default, CPTs fallback to using the “classic editor”.
In order to allow our CPT to use the block based (Gutenberg) editor, we need to explicitly tell WordPress to make the CPT accessible via the REST API. This is done by adding the setting.
'show_in_rest' => true,
Prepare Your CPT for Translation
WordPress includes functions like __() and _x(), allow a string to be localized (i.e. translated). We’ll add these functions to our label and our rewrite slug, which will have no effect at this time but will allow a translation to be created in the future.
'label' => __( 'Recipes', 'fe_recipe' )
and
'slug' => _x( 'recipes', 'CPT permalink slug', 'fe_recipe' )
Set the Rewrite with_front Value to False
The with_front
rewrite attribute is described as follows on the Codex register_post_type() entry as of 2016-07-14.
Should the permalink structure be prepended with the front base. (example: if your permalink structure is
/blog/
, then your links will be: false->/news/
, true->/blog/news/
)
Most of the sites I work on do not have a front base value, making this setting irrelevant 99% of the time. In the rare cases when I am working on a site with a front base, I have never wanted to apply it to my Custom Post Types. Since this value defaults to true
, I override it with 'with_front' => false
when I create the CPT.
Include a Menu Icon
A menu icon differentiates your CPT Menu from the others in the wp-admin area. With the built-in Dashicons, it is easy to change the icon even if you use something generic.
Example CPT Definition
This is the code I use to define the Recipes Post Type and the Taxonomy Recipe Tags on ferrarello.com.
<?php | |
/** | |
* The code to register a WordPress Custom Post Type (CPT) `fe_recipe` | |
* with a custom Taxonomy `fe_recipe_tag` | |
* @package fe_recipe | |
*/ | |
add_action( 'init', 'fe_recipe_cpt' ); | |
/** | |
* Register a public CPT and Taxonomy | |
*/ | |
function fe_recipe_cpt() { | |
// Post type should be prefixed, singular, and no more than 20 characters. | |
register_post_type( 'fe_recipe', array( | |
// Label should be plural and L10n ready. | |
'label' => __( 'Recipes', 'fe_recipe' ), | |
'public' => true, | |
'has_archive' => true, | |
'rewrite' => array( | |
// Slug should be plural and L10n ready. | |
'slug' => _x( 'recipes', 'CPT permalink slug', 'fe_recipe' ), | |
'with_front' => false, | |
), | |
// Add support for the new block based editor (Gutenberg) by exposing this CPT via the REST API. | |
'show_in_rest' => true, | |
/** | |
* 'title', 'editor', 'thumbnail' 'author', 'excerpt','custom-fields', | |
* 'page-attributes' (menu order),'revisions' (will store revisions), | |
* 'trackbacks', 'comments', 'post-formats', | |
*/ | |
'supports' => array( 'title', 'editor', 'custom-fields' ), | |
// Url to icon or choose from built-in https://developer.wordpress.org/resource/dashicons/. | |
'menu_icon' => 'dashicons-feedback', | |
) ); | |
register_taxonomy( | |
'fe_recipe_tag', | |
'fe_recipe', | |
array( | |
// Label should be plural and L10n ready. | |
'label' => __( 'Recipe Tags', 'fe_recipe' ), | |
'show_admin_column' => true, | |
'rewrite' => array( | |
// Slug should be singular and L10n ready.. | |
'slug' => _x( 'recipe-tag', 'Custom Taxonomy slug', 'fe_recipe' ), | |
), | |
) | |
); | |
} |
Flush Your Permalinks
Flush your permalinks after adding a Custom Post Type.
Hi Sal,
Thanks for all your post about CPT!
Hi Sal,
I’m building an events cpt with a few taxonomies, event-location and event-type.
I’m struggeling with activating,deactivating and uninstalling the plugin.
There are so many examples that I don’t know what to use anymore.
I have a file e.g. myplugin with myplugin.php inside containing the code for the custom post type and taxonomies
This is my code for activating/deactivating/uninstalling;
What is the “right” way to do it?
And when the plugin is uninstalled, do i lose all the data?
I’m using https://generatewp.com/ to help me out for easy setup but when I create a custom taxonomy it sets a function for each taxonomy. Do you need a function per taxonomy or can you just create multiple taxonomies under one function, do you even need to set a function for the taxonomies, you don’t use one for your taxonomy in the example above?
Hope you can help me out.
Hi Diederik,
I’ve moved my example in this post into a plugin Iron Code Recipe Custom Post Type, which includes the activation/deactivation hooks.
If you look at the plugin, you’ll see the activation/deactivation hooks. The key is in the activation hook, you need to call your function that registers your custom post type (because the activation hook executes before the custom post type is registered) – in my case this is
fe_recipe_cpt()
.Regarding adding taxonomies, you are correct you do not need additional functions to add taxonomies. If I wanted to add a second taxonomy to my recipes, I could add a second
register_taxonomy()
call insidefe_recipe_cpt()
.Thanks for the quick response, this is very helpful!
Hi Sal – really enjoyed your WordPress TV presentation so thank you for sharing … while you sold me on importance of migrating functions.php code into mu-plugins section, I was wondering how you would recommend using a WordPress Multisite with series of mu-plugins that get loaded just for specific sites and not across all sites ..??..
I saw something of a plugin that made use of sub-folders off the mu-plugins folder … each of those sub-folders corresponded to a particular site on the multisite … there seemed to be a corresponding loader php file in the mu-plugins that also corresponded to the sub-folders ..??.. but apparently that plugin is no longer available (see below) …
Thus I suspect in the wpmudev.org example below, their file, localmuplugins.php, must be some kind of loader that checks global blog_id and if found then includes the code in the corresponding mu sub-folder ..??..
Accordingly, do you have any thoughts about how best to create a file that could be placed in mu-plugins folder that checks if blog_id (and/or network id = x), then include this mu sub-folder, otherwise die and move on to next file in mu-plugins folder …
Cordially,
Chuck Scott
=============
Plugin referenced but no longer available ->
https://premium.wpmudev.org/blog/site-specific-mu-plugins/
– from the blog post, it had suggested folders like ->
mu-plugins
staypress.com
blog.clearskys.net
clearsky.net
myothersiteonthenetwork.org
localmuplugins.php
– fin –
Hi Chuck,
That is an interesting situation you’re describing.
I’m guessing
localmuplugins.php
does something like call get_blog_details() to get information about the current site.Then based on that, it loads all the files in the corresponding directory. It is probably similar to the code I use in the functions.php file of Bootstrap Genesis to load all of the files in the
/lib
directory.If you do build something like this, I hope you’ll add a link here for others looking for the same type of functionality.
Best of luck.
Hey Sal, thanks for sharing these tips!
Do you still recommend registering CPTs in an mu-plugin? One of the teams I work on was discussing today the pros and cons of that approach. On the one hand it’s clean and keeps clients (or whomever) from inadvertently deactivating a post type. But we also came up with a couple of potential cons:
1) You cannot leverage activation/deactivation hooks for flushing permalinks. Not a big deal because you just have to remember to manually save the permalinks.
2) The post types will be available on all sub-sites in a multi-site installation, which may or may not be desired.
I realize point #2 is more of an edge case :). What do you think?
Hi Tim,
You bring up some great points. Generally, my information in this post is from the perspective of building a full WordPress site. Recently, most of my work has been on plugins, in which case I define the CPTs in the plugin.
If I were building out a full website, I would still have a preference for defining my CPTs in an mu-plugin.
As you mentioned once you manually update the permalinks, you don’t have to worry about needing to do it again since an mu-plugin can not be deactivated. I think the benefits still outweigh this extra (one-time) step.
This is a good point, that I had not considered in this article. If I were to have a personal guideline, I’d say:
If there is only one site in a multi-site installation that does NOT have the CPT, I’d still define it in an mu-plugin (with code to exclude that particular site). I’ve seen situations like this where there is one parent site that is different (without the CPT) and all of the other sites are the same (with the CPT).
If more than one site in a multi-site installation does not have the CPT, I would define the CPT in a plugin.
Of course, these are just my thoughts on the subject. If your team comes up with a different set of internal practices, I’d be interested to hear more about them.
Thanks Sal for all of your Tips!
Luckly I´ve found your blog =)
it comes to my mind the “cpt_products” which is very Worn Out.
Thank you for this detailed guide. Can you help me to include Gutenberg editor using custom post type? I am trying to add an advertising portfolio but I am having an issue while implementing the code. I am having an unknown error. Can you help me out in this code as I have seen this from the tutorial https://www.cloudways.com/blog/gutenberg-wordpress-custom-post-type/
Hi Alex,
I’ve updated this post with information on how to add Support for the Block Based Editor (Gutenberg).
The key is adding the setting
Hi Sal,
when would you use __() and when _x()?
Thanks
Tim
Hi Tim,
The function _x() does the same thing as the function __() except the
_x()
function accepts an additional parameter ($context
).I’ve written a post about the difference between __() and _x() in WordPress.