How to use WordPress custom post types

While WordPress’s available posts and pages cover most needs for static websites and blogs, custom post types can transform your clients’ site into a more robust content management system (CMS). 

Additionally, the combination of block templates and custom post types (CPT) make it easier than ever for your clients to update their own sites, without f**king up your design.

What is a custom post type (CPT)?

Think about the parts that make up a standard post on WordPress. Now, imagine that all those components were customizable and could be expanded to fit any type of content. That’s how custom post types work. From its earliest days, WordPress already had a few post types built in, including posts, pages, attachments, and menus, but version 2.9 opened up this structure up to everyone allowing a greater level of customization to the platform.

Are custom post types (CPT) the right move for your project?

Of course, custom post types are not always the best solution for every clients’ request. Often a content addition can be resolved by creating a landing page, using a custom page template, installing a plugin, or sprucing up an existing archive template, but other times you need a more robust solution. 

Ask yourself this:

  • Is the content repeatable? A CPT is overkill for a one-off request and should only be used when a similar type of content needs to be repeated indefinitely.
  • Can the content be templatized? CPT is best for content which is going to be displaying the same variable dataset.
  • Do you want a dynamic and dedicated browsing experience for this content? You’ll often hear clients ask for a page where users can browse the latest from only this specific content type, as well as use the words “library,” “reference list,” “calendar,” or “portfolio” in describing their needs.

If you answered yes to any of these, then CPT may be the best tool to keep that content looking tidy.

3 Examples of WordPress Custom Post Types (CPT) in the Wild

WordPress custom post types are everywhere. And when I say everywhere, I mean an incredibly large majority of WordPress plugins are built using CPT, ergo a large amount of WP sites have some form of CPT in place whether they know it or not.

1. Jetpack, built by Automattic

The settings are toggled off by default, but the Jetpack plugin comes with a Portfolio and Testimonial CPT built-in. Check it out in action on the Unwind theme demo.

jetpack portfolio and testimonial custom post type

2. WooCommerce

When you install WooCommerce on your site, your entire store is run off of a Products CPT. Orange Amps runs their shop through this plugin.

woocommerce products custom post type

3. HostGator’s Resource Library

Our own Resource Library at HostGator was built using a Resources CPT and offers a dedicated browsing experience for long-living educational content, like email courses, ebooks and webinars. 

hostgator blog example wordpress custom post type

How to Build Your Own WordPress Custom Post Type Plugin

For the sake of this tutorial, let’s say you’ve got a client who livestreams lore-heavy Dungeons & Dragons campaigns and they’re looking for a way to create a library of recurring game characters. They also want to enable players and guest players to go in and update their own character pages. 

This is a great opportunity to use CPT because the content is repeatable, can be templatized, and would benefit from a dynamic and dedicated browsing experience.

I’m going to walk you through building your own WordPress CPT, from brainstorm to build.

Before You Do Anything Else, Make a Plan for Your CPT

You know what is sick? Knowing what the hell you’re doing before getting elbows deep in building it, realizing you didn’t bring a compass.

Each CPT can come with its own browsing experience, including its own taxonomies and archive landing pages and singular pages. You can inherit a lot from your existing theme set-up or create an entirely new browsing experience for your CPT. In this tutorial, we’ll be focusing on setting up a basic CPT with a custom single page block template.

Before you code, you’ve got to plan. There are three things to consider during the planning phase when creating a CPT:

  • What data will you be using?
  • What should it look like?
  • Which WordPress blocks will you use?

Let’s dig in.

1. What’s the data?

Talk to your client to get an idea about what information they are wanting to display. For this example of the Dungeons & Dragons character library, I want to display the following information from character sheets:

  • Character’s Name
  • Player’s Name
  • Level
  • Race
  • Class
  • Character Sketch
  • Backstory
  • Alignment
  • Background
  • Age
  • Height
  • Weight
  • Eye Color
  • Hair Color

2. What should it look like? 

Wireframe the pages so you know where and how the data will need to be pulled into the front-end. It doesn’t need to be complicated, just a basic page layout for where this information will show up.

wireframe for custom post type in wordpress

3. Which blocks are we using?

In November 2020, WordPress released Block Patterns and Block Templates, which makes CPT page building easier than ever.

Many of the features of a standard post, e.g. post title and content, are available by default through the custom post type. 

Other information can be added to our template via an array of block variables. For this example, I’m sticking with WordPress core blocks, but you can use blocks from any plugin’s block library.

  • Character’s Name => post_title
  • Player’s Name => author
  • Nested columns => core/columns
    • Level => core/paragraph
    • Race => core/paragraph
    • Class => core/paragraph
  • Nested columns => core/columns
    • Character Sketch => core/image
    • Traits => core/heading
      • Alignment => core/paragraph
      • Background => core/paragraph
      • Age => core/paragraph
      • Height => core/paragraph
      • Weight => core/paragraph
      • Eye Color => core/paragraph
      • Hair Color => core/paragraph
  • Backstory => core/paragraph

How to Code Your WordPress CPT, Step by Step

CPTs can be implemented as part of your custom theme or as a plugin. For this tutorial, I decided to focus on building it out as a standalone plugin, as that will be most useful to those using pre-built or child themes and those who have a custom built WordPress theme.

Step 1: Bootstrap your Plugin

Starting with a boilerplate plugin takes the guesswork out of getting everything set up in an object-oriented framework. This will save you a lot of time and help keep your code organized.

My boilerplate of choice, WPPB.me, is super easy-to-use and very clearly documented. 

wordpress plugin boilerplate generator

If I was to build out our Dungeons & Dragons CPT using WPPB.me, here are the steps I’d follow:

  1. Complete the form. I’ll call this plugin “The Tavern” because it’s where all the characters are gathered under one roof, and I’ll set the slug to tavern to keep things easy. Fill out the rest of the form fields with your information and click “Build Plugin” to download.
  2. After unzipping the file, you’ll want to go into the folder and open tavern.php, and update the plugin description on line 18. This is the description that shows up on the Plugins screen. 
  3. Upload the file into the wp-content/plugins directory on your test environment, you should see The Tavern in the Plugins section in the WP admin dashboard.

Step 2: Register your Custom Post Type

The next thing you’ll want to do is set up your Characters custom post type.

Get into the Admin File

Open tavern/admin/class-tavern-admin.php.

This is the file where you put any functions that can be called on the admin side. The first function we’ll be setting up is the register_post_type function. You can find full details of this function and its various attributes on the WordPress.org Code Reference

Define the Function

This will be your wrapper for the register_post_type function, which you’ll call in the init action. You can name it whatever you want (using underscores), but I prefer using something descriptive, like register_characters_cpt.

/**
 * Register the characters custom post type
 *
 * @since    1.0.0
 */
public function register_characters_cpt() {
     /** start coding here **/
}

Labels

The $labels variable tells WordPress which words you are wanting to replace in the common post editing experience. You’ll notice that I am using the __() and _x() functions to input text into these labels. This is to keep the plugin open for localization (l10n), which will make translation easier in the future.

[code snippet 2.2]

$labels = array(
    'name'                  => _x( 'Characters', 'Post Type General Name', 'tavern' ),
    'singular_name'         => _x( 'Character', 'Post Type Singular Name', 'tavern' ),
    'menu_name'             => __( 'The Tavern', 'tavern' ),
    'name_admin_bar'        => __( 'Character Sheets', 'tavern' ),
    'archives'              => __( 'Our Characters', 'tavern' ),
    'attributes'            => __( 'Character Attributes', 'tavern' ),
    'parent_item_colon'     => __( 'Parent Character:', 'tavern' ),
    'all_items'             => __( 'All Characters', 'tavern' ),
    'add_new_item'          => __( 'Add New Character', 'tavern' ),
    'add_new'               => __( 'Add New', 'tavern' ),
    'new_item'              => __( 'New Character', 'tavern' ),
    'edit_item'             => __( 'Edit Character', 'tavern' ),
    'update_item'           => __( 'Update Character', 'tavern' ),
    'view_item'             => __( 'View Character', 'tavern' ),
    'view_items'            => __( 'View Characters', 'tavern' ),
    'search_items'          => __( 'Search Characters', 'tavern' ),
    'not_found'             => __( 'Not found', 'tavern' ),
    'not_found_in_trash'    => __( 'Not found in Trash', 'tavern' ),
    'featured_image'        => __( 'Character Sketch', 'tavern' ),
    'set_featured_image'    => __( 'Set character sketch', 'tavern' ),
    'remove_featured_image' => __( 'Remove character sketch', 'tavern' ),
    'use_featured_image'    => __( 'Use as character sketch', 'tavern' ),
    'insert_into_item'      => __( 'Insert into character sheet', 'tavern' ),
    'uploaded_to_this_item' => __( 'Uploaded to this character sheet', 'tavern' ),
    'items_list'            => __( 'Character list', 'tavern' ),
    'items_list_navigation' => __( 'Character list navigation', 'tavern' ),
    'filter_items_list'     => __( 'Filter character list', 'tavern' ),
);

Arguments

The $args variable tells WordPress how to set up the custom post type and how it will interact with the admin and front-end experiences on the site. Again, the Code Reference is a great resource for better understanding what each of these attributes do.

$args = array(
    'label'                 => __( 'Character', 'tavern' ),
    'description'           => __( 'Stats and Lore about our Characters', 'tavern' ),
    'labels'                => $labels,
    'supports'              => array( 'title', 'author', 'editor', 'thumbnail', 'comments', 'trackbacks' ),
    'hierarchical'          => false,
    'public'                => true,
    'show_ui'               => true,
    'show_in_menu'          => true,
    'menu_position'         => 5,
    'menu_icon'             => 'dashicons-beer',
    'show_in_admin_bar'     => true,
    'show_in_nav_menus'     => true,
    'can_export'            => true,
    'has_archive'           => true,
    'exclude_from_search'   => false,
    'publicly_queryable'    => true,
    'capability_type'       => 'post',
    'show_in_rest'	    => true,
    'rest_base'		    => 'tavern-api',
    'rest_controller_class' => 'WP_REST_Posts_Controller',
);

Putting it All Together

Now that we’ve defined all the variables, adding the register_post_type function is easy.

/**
* Register the characters custom post type
*
* @since    1.0.0
*/
public function register_characters_cpt() {

    $labels = array(
        'name'                  => _x( 'Characters', 'Post Type General Name', 'tavern' ),
        'singular_name'         => _x( 'Character', 'Post Type Singular Name', 'tavern' ),
        'menu_name'             => __( 'The Tavern', 'tavern' ),
        'name_admin_bar'        => __( 'Character Sheets', 'tavern' ),
        'archives'              => __( 'Our Characters', 'tavern' ),
        'attributes'            => __( 'Character Attributes', 'tavern' ),
        'parent_item_colon'     => __( 'Parent Character:', 'tavern' ),
        'all_items'             => __( 'All Characters', 'tavern' ),
        'add_new_item'          => __( 'Add New Character', 'tavern' ),
        'add_new'               => __( 'Add New', 'tavern' ),
        'new_item'              => __( 'New Character', 'tavern' ),
        'edit_item'             => __( 'Edit Character', 'tavern' ),
        'update_item'           => __( 'Update Character', 'tavern' ),
        'view_item'             => __( 'View Character', 'tavern' ),
        'view_items'            => __( 'View Characters', 'tavern' ),
        'search_items'          => __( 'Search Characters', 'tavern' ),
        'not_found'             => __( 'Not found', 'tavern' ),
        'not_found_in_trash'    => __( 'Not found in Trash', 'tavern' ),
        'featured_image'        => __( 'Character Sketch', 'tavern' ),
        'set_featured_image'    => __( 'Set character sketch', 'tavern' ),
        'remove_featured_image' => __( 'Remove character sketch', 'tavern' ),
        'use_featured_image'    => __( 'Use as character sketch', 'tavern' ),
        'insert_into_item'      => __( 'Insert into character sheet', 'tavern' ),
        'uploaded_to_this_item' => __( 'Uploaded to this character sheet', 'tavern' ),
        'items_list'            => __( 'Character list', 'tavern' ),
        'items_list_navigation' => __( 'Character list navigation', 'tavern' ),
        'filter_items_list'     => __( 'Filter character list', 'tavern' ),
    );

    $args = array(
        'label'                 => __( 'Character', 'tavern' ),
        'description'           => __( 'Stats and Lore about our Characters', 'tavern' ),
        'labels'                => $labels,
        'supports'              => array( 'title', 'author', 'editor', 'thumbnail', 'comments', 'trackbacks' ),
        'hierarchical'          => false,
        'public'                => true,
        'show_ui'               => true,
        'show_in_menu'          => true,
        'menu_position'         => 5,
        'menu_icon'             => 'dashicons-beer',
        'show_in_admin_bar'     => true,
        'show_in_nav_menus'     => true,
        'can_export'            => true,
        'has_archive'           => true,
        'exclude_from_search'   => false,
        'publicly_queryable'    => true,
        'capability_type'       => 'post',
        'show_in_rest'			=> true,
        'rest_base'				=> 'tavern-api',
        'rest_controller_class'	=> 'WP_REST_Posts_Controller',
    );
    register_post_type( 'characters', $args );

}

Calling the Action

Open tavern/includes/class-tavern.php. This is the file where all your actions, hooks, and filters are called. Instead of calling the init action directly under our function, we’re keeping things tidy by calling all of our actions in the same place. At line 153, you’ll see a define_admin_hooks() function, to which you will add the following code snippet.

$this->loader->add_action( 'init', $plugin_admin, 'register_characters_cpt' );

Activate the Plugin

Navigate to the Plugins screen in your WordPress admin dashboard and activate the plugin. Once activated, you’ll see The Tavern pop up in your left hand menu. 

activate wordpress plugin

Step 3: Set-up the Block Template

Remember how we set up those wireframes earlier? Well, it’s time to get those out and start working on adding our first character into The Tavern. This section will have you switching between the dashboard and your plugin code pretty regularly. Just follow these steps.

1. Add a New Character

When you navigate to The Tavern > Add New, a page will pop up that looks like your standard post editing experience. 

add new page in wordpress

Using the block system, frame out a default character sheet from the Edit screen. This should match your wireframes.

build custom post type in wordpress

Then click the three dots in the top right corner to switch to Code Editor. This will be a helpful reference when you’re coding the template into your plugin.

how to code wordpress custom post type

2. Define the Function

Open tavern/admin/class-tavern-admin.php.

You can add the block template code in through your existing register_post_type function, but I’d recommend putting this into its own function to keep it from turning into spaghetti. 

/**
* Set up the character block template
*
* @since    1.0.0
*/
public function register_character_template() {
    /** start coding here **/
}

3. Set up the Template

Nested block template arrays can get a little hairy, so it’s best to build it out in sections to avoid having to chase down errant closing tags. 

For starters, the breakdown of a block array is as follows:

array( ‘block-library/block-name’, array( ‘content’ => ‘and other properties’ ) )

Nested blocks have an additional array to hold the blocks contained within and follow this format:

array( 'core/columns', array($props), array(
  array( 'core/column', array($props), array($contents) ),
  array( 'core/column', array($props), array($contents) ),
) );

First, frame out your columns and full-width blocks.

$template = array(
    array( 'core/columns', array(), array(
        array( 'core/column', array(), array(
            array( 'core/columns', array(), array(
                array( 'core/column', array(), array() ),
                array( 'core/column', array(), array() ),
                array( 'core/column', array(), array() ),
            ) ),
        ) ),
        array( 'core/column', array(), array() ),
    ) ),
    array( 'core/heading', array() ),
    array( 'core/columns', array(), array(
        array( 'core/column', array(), array() ),
        array( 'core/column', array(), array(
            array( 'core/columns', array(), array(
                array( 'core/column', array(), array() ),
                array( 'core/column', array(), array() ),
            ) ),
        ) ),
    ) ),
    array( 'core/heading', array() ),
    array( 'core/paragraph', array() ),
);

Next, add your nested blocks in. 

$template = array(
    array( 'core/columns', array(), array(
        array( 'core/column', array(), array(
            array( 'core/columns', array(), array(
                array( 'core/column', array(), array(
                    array( 'core/paragraph', array() ),
                ) ),
                array( 'core/column', array(), array(
                    array( 'core/paragraph', array() ),
                ) ),
                array( 'core/column', array(), array(
                    array( 'core/paragraph', array() ),
                ) ),
            ) ),
        ) ),
        array( 'core/column', array(), array(
            array( 'core/paragraph', array() ),
        ) ),
    ) ),
    array( 'core/heading', array() ),
    array( 'core/columns', array(), array(
        array( 'core/column', array(), array(
            array( 'core/heading', array() ),
            array( 'core/image', array() ),
        ) ),
        array( 'core/column', array(), array(
            array( 'core/heading', array() ),
            array( 'core/columns', array(), array(
                array( 'core/column', array(), array(
                    array( 'core/paragraph', array() ),
                    array( 'core/paragraph', array() ),
                    array( 'core/paragraph', array() ),
                    array( 'core/paragraph', array() ),
                ) ),
                array( 'core/column', array(), array(
                    array( 'core/paragraph', array() ),
                    array( 'core/paragraph', array() ),
                    array( 'core/paragraph', array() ),
                ) ),
            ) ),
        ) ),
    ) ),
    array( 'core/heading', array() ),
    array( 'core/paragraph', array() ),
);

Then, add additional properties. You can find the properties of the additional attributes by checking the Code Editor on the test build you did through the dashboard. Some additional properties you won’t find there are content and placeholder for defining the text within the block on load.

$template = array(
   array( 'core/columns', array(), array(
       array( 'core/column', array(), array(
           array( 'core/columns', array(
               'className' => 'base-stats',
           ), array(
               array( 'core/column', array(), array(
                   array( 'core/paragraph', array(
                       'content' => 'Lvl #',
                   ) ),
               ) ),
               array( 'core/column', array(), array(
                   array( 'core/paragraph', array(
                       'placeholder' => 'race',
                   ) ),
               ) ),
               array( 'core/column', array(), array(
                   array( 'core/paragraph', array(
                       'placeholder' => 'class',
                   ) ),
               ) ),
           ) ),
       ) ),
       array( 'core/column', array(), array(
           array( 'core/paragraph', array(
               'backgroundColor' => 'purple',
               'content' => '<em>Player:</em> Your Name',
           ) ),
       ) ),
   ) ),
   array( 'core/heading', array(
       'content' => 'Character Details',
   ) ),
   array( 'core/columns', array(), array(
       array( 'core/column', array(
           'width' => '33.33%'
       ), array(
           array( 'core/heading', array(
               'level' => 3,
               'content' => 'Appearance',
           ) ),
           array( 'core/image', array(
               'className' => 'is-style-twentytwentyone-image-frame',
           ) ),
       ) ),
       array( 'core/column', array(
           'width' => '66.66%',
       ), array(
           array( 'core/heading', array(
                'level' => 3,
               'content' => 'Traits',
           ) ),
           array( 'core/columns', array(), array(
                array( 'core/column', array(), array(
                    array( 'core/paragraph', array(
                        'content' => '<strong>Alignment:</strong> true neutral',
                        'fontSize' => 'extra-small',
                    ) ),
                    array( 'core/paragraph', array(
                        'content' => '<strong>Age:</strong> ##',
                        'fontSize' => 'extra-small',
                    ) ),
                    array( 'core/paragraph', array(
                        'content' => '<strong>Height:</strong> #\'#"',
                        'fontSize' => 'extra-small',
                    ) ),
                    array( 'core/paragraph', array(
                        'content' => '<strong>Weight:</strong> ###lbs',
                        'fontSize' => 'extra-small',
                    ) ),
                ) ),
                array( 'core/column', array(), array(
                    array( 'core/paragraph', array(
                        'content' => '<strong>Background:</strong> bg',
                        'fontSize' => 'extra-small',
                    ) ),
                    array( 'core/paragraph', array(
                        'content' => '<strong>Eye Color:</strong> color',
                        'fontSize' => 'extra-small',
                    ) ),
                    array( 'core/paragraph', array(
                        'content' => '<strong>Hair Color:</strong> color',
                        'fontSize' => 'extra-small',
                    ) ),
                ) ),
            ) ),
       ) ),
   ) ),
   array( 'core/heading', array(
       'content' => 'Character Backstory',
    ) ),
   array( 'core/paragraph', array(
       'placeholder' => 'Enter your character\'s backstory...',
   ) ),
);

4. Update the Characters Object and Lock the Template

With the template defined, we’ll tell WordPress to update the template for the characters post type and add a property to lock the template. Locking the template keeps the block layout from being manipulated (and keep the design from getting f*cked up).

/**
* Set up the character block template
*
* @since    1.0.0
*/
public function register_character_template() {
    $template = array(
        array( 'core/columns', array(
            'verticalAlignment' => 'center',
        ), array(
            array( 'core/column', array(), array(
                array( 'core/columns', array(
                    'className' => 'base-stats',
                ), array(
                    array( 'core/column', array(), array(
                        array( 'core/paragraph', array(
                            'content' => 'Lvl #',
                        ) ),
                    ) ),
                    array( 'core/column', array(), array(
                        array( 'core/paragraph', array(
                            'placeholder' => 'race',
                        ) ),
                    ) ),
                    array( 'core/column', array(), array(
                        array( 'core/paragraph', array(
                            'placeholder' => 'class',
                        ) ),
                    ) ),
                ) ),
            ) ),
            array( 'core/column', array(), array(
                array( 'core/paragraph', array(
                    'backgroundColor' => 'purple',
                    'content' => '<em>Player:</em> Your Name Here',
                ) ),
            ) ),
        ) ),
        array( 'core/heading', array(
            'content' => 'Character Details',
        ) ),
        array( 'core/columns', array(), array(
            array( 'core/column', array(
                'width' => '33.33%',
            ), array(
                array( 'core/heading', array(
                    'level' => 3,
                    'content' => 'Appearance',
                ) ),
                array( 'core/image', array(
                    'className' => 'is-style-twentytwentyone-image-frame',
                ) ),
            ) ),
            array( 'core/column', array(
                'width' => '66.66%',
            ), array(
                array( 'core/heading', array(
                    'level' => 3,
                    'content' => 'Traits',
                ) ),
                array( 'core/columns', array(
                    'className' => ''
                ), array(
                    array( 'core/column', array(), array(
                        array( 'core/paragraph', array(
                            'content' => '<strong>Alignment:</strong> true neutral',
                            'fontSize' => 'extra-small',
                        ) ),
                        array( 'core/paragraph', array(
                            'content' => '<strong>Age:</strong> ##',
                            'fontSize' => 'extra-small',
                        ) ),
                        array( 'core/paragraph', array(
                            'content' => '<strong>Height:</strong> #\'#"',
                            'fontSize' => 'extra-small',
                        ) ),
                        array( 'core/paragraph', array(
                            'content' => '<strong>Weight:</strong> ###lbs',
                            'fontSize' => 'extra-small',
                        ) ),
                    ) ),
                    array( 'core/column', array(), array(
                        array( 'core/paragraph', array(
                            'content' => '<strong>Background:</strong> bg',
                            'fontSize' => 'extra-small',
                        ) ),
                        array( 'core/paragraph', array(
                            'content' => '<strong>Eye Color:</strong> color',
                            'fontSize' => 'extra-small',
                        ) ),
                        array( 'core/paragraph', array(
                            'content' => '<strong>Hair Color:</strong> color',
                            'fontSize' => 'extra-small',
                        ) ),
                    ) ),
                ) ),
            ) ),
        ) ),
        array( 'core/heading', array(
            'content' => 'Character Backstory',
        ) ),
        array( 'core/paragraph', array(
            'placeholder' => 'Enter your character\'s backstory...',
        ) ),
    );
    $post_type_object = get_post_type_object('characters');
    $post_type_object->template = $template;
    $post_type_object->template_lock = 'all';
}

5. Call the Action

Open tavern/includes/class-tavern.php. We’ll add this new action right under our register_characters_cpt action. 

$this->loader->add_action( 'init', $plugin_admin, 'register_character_template' );

6. Test it Out

Go ahead and add a new character in from the admin dashboard. All of the blocks on the page should be laid out as you set them up in your block template array.

wordpress custom post type

Wrapping up

After testing, you should be able to install your CPT on your client’s site and begin building out their starter library. It is usually helpful to have a walkthrough meeting with them to give them a short training session on adding and updating their library.

This tutorial barely scratches the surface of what can be done. WordPress custom post types are capable of so much more, like creating custom library pages for enhanced browsing or setting up custom taxonomies allowing for even better organization. Perhaps we’ll go into that at a later date, but this is a good start.

Armed with this new CPT knowledge in your tool box, we’re hoping you’ll be confident to do some more exploring. Start building today with WordPress hosting from HostGator.

Samantha Soper is a freelance UX strategist, front-end developer, and creative consultant, working with businesses and non-profits of all sizes to help manage their online presence and brand experience. She also runs a mural art and illustration business. Related links at: samsoper.art/links/