• Skip to primary navigation
  • Skip to main content
Sal Ferrarello
  • About Sal Ferrarello
  • Speaking
  • Connect
    Mastodon GitHub Twitter (inactive)
You are here: Home / Programming / WordPress CPT Best Practices

WordPress CPT Best Practices

Last updated on January 13, 2021 by Sal Ferrarello

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 to false
  • 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 Dashicon Custom Post Type Menu Icon

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' ),
),
)
);
}
view raw cpt-fe-recipe.php hosted with ❤ by GitHub

Flush Your Permalinks

Flush your permalinks after adding a Custom Post Type.

Image Credit

Pixabay

Sal Ferrarello
Sal Ferrarello (@salcode)
Sal is a PHP developer with a focus on the WordPress platform. He is a conference speaker with a background including Piano Player, Radio DJ, Magician/Juggler, Beach Photographer, and High School Math Teacher. Sal can be found professionally at WebDevStudios, where he works as a senior backend engineer.

Share this post:

Share on TwitterShare on FacebookShare on LinkedInShare on EmailShare on Reddit

Filed Under: Programming Tagged With: CPT, WordPress

Reader Interactions

Comments

  1. Diederik says

    April 11, 2017 at 1:29 pm

    Hi Sal,

    Thanks for all your post about CPT!

    Reply
  2. Diederik says

    April 13, 2017 at 6:39 pm

    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;

    
    // Perform Uninstall hook inside register_activation_hook
    function ot_uitjes_activate(){
        register_uninstall_hook( __FILE__, 'ot_uitjes_uninstall' );
    }
    register_activation_hook( __FILE__, 'ot_activate' );
    function ot_uitjes_uninstall(){
    }
    function ot_uitjes_deactivation() { 
        // clear the permalinks to remove post type's rules
        flush_rewrite_rules();
    }
    register_deactivation_hook( __FILE__, 'ot_uitjes_deactivate' );
    

    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.

    Reply
    • Sal Ferrarello says

      April 14, 2017 at 12:10 pm

      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 inside fe_recipe_cpt().

      Reply
      • Diederik says

        April 14, 2017 at 12:20 pm

        Thanks for the quick response, this is very helpful!

        Reply
  3. Chuck Scott says

    February 15, 2018 at 4:36 pm

    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 –

    Reply
    • Sal Ferrarello says

      February 16, 2018 at 1:04 pm

      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.

      
      foreach ( glob( dirname( __FILE__ ) . '/lib/*.php' ) as $file ) { include $file; }
      

      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.

      Reply
  4. Tim says

    May 15, 2018 at 5:19 pm

    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?

    Reply
    • Sal Ferrarello says

      May 16, 2018 at 10:46 am

      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.

      You cannot leverage activation/deactivation hooks for flushing permalinks.

      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.

      The post types will be available on all sub-sites in a multi-site installation, which may or may not be desired.

      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.

      Reply
  5. Vale23 says

    September 1, 2018 at 2:43 pm

    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.

    Reply
  6. Alex says

    February 22, 2019 at 5:56 am

    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/

    
    function cw_post_type() {
        register_post_type( 'portfolio',
            array(
                'labels' => array(
                    'name' => __( 'Portfolio' ),
                    'singular_name' => __( 'Portfolio' )
                ),
                'has_archive' => true,
                'public' => true,
                'rewrite' => array('slug' => 'portfolio'),
            )
        );
    }
    
    add_action( 'init', 'cw_post_type' );
    
    Reply
    • Sal Ferrarello says

      March 7, 2019 at 3:03 pm

      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

      'show_in_rest' => true,
      Reply
  7. Tim Kaufmann says

    May 20, 2019 at 6:41 pm

    Hi Sal,

    when would you use __() and when _x()?

    Thanks

    Tim

    Reply
    • Sal Ferrarello says

      May 21, 2019 at 7:09 am

      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.

      Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Copyright © 2023 · Bootstrap4 Genesis on Genesis Framework · WordPress · Log in