Skip to main content
Code Tutorial

Ultimate Guide to Custom Post Types in WordPress

Contents

In WordPress, sometimes you need more than "Posts" and "Pages" post types to organize content under. After all, not every WordPress site is just a blog. You need the ability to add new types of content. So, how do you craft custom types of content in WordPress?

The answer to this question is found with WordPress post types. Learning the full capabilities of post types can turn your site's admin area into an organized, exciting, and beautiful place. Ultimately, you want your WordPress site to feel empowering when creating new content.

WordPress post types make it easy to organize your content by letting you create new buckets to place your own types of content into so you can have a beautiful site.

So, what are post types like practically?

What is a Post Type?

Simply put, a post type works just like the "Pages" and "Posts" in the WordPress admin. When you create a post type it will be listed in the admin menu and have the editor and title fields just like "Posts". Once you have it in place, you can add content to it in the same way you add a blog "Post" to the "Posts" section of the admin.

Once you learn more, you will be able to make your post type do much more than "Pages" and "Posts". On top of all this, you can customize how the front-end of your site displays the content you add to post types.

Your new post type might be for "Movies", "Reviews", "Homes", or "Case Studies". No matter what new type of content you want to create, the WordPress post type functionality makes it possible.

While there are plugins for creating post types, you do not want a plugin controlling your design. You certainly do not want any chance of a plugin update breaking your design. You want total control over the user experience. For this reason, coding your post types is the right answer to crafting an organized, exciting, and beautiful content management experience.

The fastest way to register, add, or make a post type is to code one into your theme. Designers and developers can add post types using the WordPress register_post_type function. This allows you to start adding content to the new post type bucket right away and display it on your website.

As you learn more about equipping post types, you will be able to build an amazing admin experience. Ready to get started? It will only take you five lines of code. Now, that is good news! 🙌

A First and Fast Look at a Post Type

content-image-admin-make-post-type

With a sense of what a post type is, let's get into action. At a minimum, to make a post type follow these steps:

  1. Login to your WordPress site's admin in your web browser.
  2. Open up your favorite code editor and open your theme's functions.php file.
  3. Add the following five lines of code to the top of your functions.php file.
  4. Refresh your browser.
  5. Look in the admin menu under "Comments" for your new post type menu item.
  6. Finally, flush the WordPress permlinks
add_action( 'init', function() {
    $label = 'Books';
    $type = 'book';
    register_post_type( $type, [ 'public' => true, 'label'  => $label ] );
});

That's it. Just like that, you have your first post type. Congrats!

Now, when you start using your post type, you might wonder 🤔 "This is great... but, this doesn't seem amazing to me. Things look a bit strange, and I need more control! Can I do more?"

YES! If you dare. Take a walk with me and let's explore the full capabilities and control you can possess with post types and move toward a more refined experience.

A Comprehensive WordPress Post Type: "Study"

content-image-make-post-type-example-2

Our example from before was a great sample, but let's take a peek at a complete picture. Something that we might do in the real world. Making a portfolio website that needs to list case studies is a great place to start. In this case, we will want to register a post type to manage each "Study".

Using the Twenty Seventeen WordPress theme as our starting block (you can use the Twenty Twenty WordPress theme if you like), let's add our "Study" post type right into the theme. Open the themes functions.php file in your favorite editor because we will be writing all of the post type code into the functions.php file. At the top of functions.php you need to register your post type first. You can do this with the init WordPress hook using add_action(). When using the register_post_type function to add your post type it must be coded within the init hook to work properly.

There are three arguments for the register_post_type you will want to start with:

  1. public - Make the post type accessible to everyone.
  2. description - Though the description is not used much, it is nice to have.
  3. label - The label of the post type. We will expand on this later.
add_action( 'init', function() {
    $type = 'study';
    $label = 'Studies';
    $arguments = [
        'public' => true, // Allow access to post type
        'description' => 'Case studies for portfolio.', // Add a description
        'label'  => $label // Set the primary label
    ];
    register_post_type( $type, $arguments);
});

Now, be sure you flush the WordPress permlinks anytime you complete changes to a post type. If you modified yours, do that before moving forward.

With our new post type in place, let's fix all the places and labels that say "Post" instead of "Study". We need great design.

Admin: Labeling

Let's customize our post type labels within the admin. By default, WordPress will label our new post type as "Post" almost everywhere.

We need the labels to be primarily marked as "Study", not "Post".

To override the default labels for our post type, we need to set the label argument from before to an array of named key pairs. In other words, we need to specify a few labels manually.

To set the labels for English readers, let's create a function that compiles the labels without having to clutter up our post type registration code.

Add this function to your theme so you can use it within the post type registration process.

function xcompile_post_type_labels($singular = 'Post', $plural = 'Posts') {
    $p_lower = strtolower($plural);
    $s_lower = strtolower($singular);

    return [
        'name' => $plural,
        'singular_name' => $singular,
        'add_new_item' => "New $singular",
        'edit_item' => "Edit $singular",
        'view_item' => "View $singular",
        'view_items' => "View $plural",
        'search_items' => "Search $plural",
        'not_found' => "No $p_lower found",
        'not_found_in_trash' => "No $p_lower found in trash",
        'parent_item_colon' => "Parent $singular",
        'all_items' => "All $plural",
        'archives' => "$singular Archives",
        'attributes' => "$singular Attributes",
        'insert_into_item' => "Insert into $s_lower",
        'uploaded_to_this_item' => "Uploaded to this $s_lower",
    ];
}

Finally, back in the registration code, call the function we created xcompile_post_type_labels() and use that for your labels instead and check your admin.

add_action( 'init', function() {
    $type = 'study';

    // Call the function and save it to $labels
    $labels = xcompile_post_type_labels('Study', 'Studies');

    $arguments = [
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'labels'  => $labels // Changed to labels
    ];
    register_post_type( $type, $arguments);
});

Nice. Now we are talking! 👍 (If you want to customize the alert messages and the labels, hold on, we will cover them soon.)

However, the menu icon... Can't that be better? Who wants to reuse the pin icon?!

Admin: Menu Icon and Position for a Post Type

content-image-make-post-type-example-icons-1

Since the introduction of the WP Dashicons, adding menu icons to post types has become very simple.

Set the menu_icon argument to one of the Dashicon names. For our post type, we can use the computer desktop icon by using the value dashicons-desktop.

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    $arguments = [
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop', // Set icon
        'labels'  => $labels
    ];
    register_post_type( $type, $arguments);
});

Also, if we want wanted to change where the menu item is located we can use the menu_position argument. menu_position takes a value between 0 and 100 and will list the menu item up and down the menu base on the value 0 being the top and 100 is the bottom. However, we do not need to do that here.

Okay, things are starting to take shape and look great. However, having a featured image for the study would bring us closer to great design and makes a lot of sense for a "Study". Only, how do you add a featured image field?

Admin: Enabling and Disabling the Featured Image, Title, Editor, and More

When you want to change or add to the default WordPress admin form fields, like the "Featured Image" and "Title" and "Editor" fields, on your post type you need to use set supports argument.

There are several options you can enable and disable using the post type supports argument:

  • title - For the title field.
  • editor - For the WordPress editor.
  • author - For the author meta box.
  • thumbnail - For the featured image.
  • excerpt - For the excerpt of text on an archive page for example.
  • trackbacks - For the trackback meta box.
  • custom-fields - For the custom fields.
  • comments - For comments.
  • revisions - For revisions and restoring old versions of the editor field.
  • page-attributes - For enabling the menu order and parent content when the post types hierarchy is enabled.
  • post-formats - For enabling the different post formats.

For the "Study" post type, we can override the WordPress default supported options and add a "Featured Image" by applying the thumbnail option.

Note that we must enable theme support for post-thumbnails using the add_theme_support function so we can use the thumbnail.

Using the support feature is a little more complicated, but it works out great. Plus, it will only cost us three more lines of code, and it enables the "Featured Image".

// Add theme support for featured image / thumbnails
add_theme_support('post-thumbnails');

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    // Declare what the post type supports
    $supports = ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'];

    $arguments = [
        'supports' => $supports, // Apply supports
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'labels'  => $labels,
    ];
    register_post_type( $type, $arguments);
});

You are crushing it now. Not too many designers or developers make it this far, but you are not most. You care about all the details, including the title placeholder text that says "Enter title here".

As they say, the devil is in the details. 😈

Admin: Change a Post Types Title Placeholder Text

You do not always want the placeholder text for a post type to be "Enter title here". To change it you need to use the WordPress enter_title_here hook using the function add_filter.

For the "Study" post type, we can set the title fields placeholder text "Enter name of the client here". This will enhance our design by adding clarity. We want users to feel empowered, not confused.

add_filter( 'enter_title_here', function( $title ) {
    $screen = get_current_screen();

    if  ( 'study' == $screen->post_type ) {
        $title = 'Enter name of the client here';
    }

    return $title;
} );

Changing the placeholder text is a great way to remind you what the title information should be, and it is kind to be clear. Still, is there a way to be more helpful? Many times our user wants to be a hero but needs a guide.

By adding help tabs, we can direct the user when they need assistance. Now that is empowering!

Admin: Adding a Help Tab to a Post Type

To add a help tab to a post type use the admin_head hook using the function add_action. We just need to make sure our help tab only appears for our "Study" post type.

content-image-make-post-type-example-help-1

add_action('admin_head', function() {
    $screen = get_current_screen();

    if ( 'study' != $screen->post_type )
        return;

    $args = [
        'id'      => 'study_help',
        'title'   => 'Study Help',
        'content' => '<h3>Case Study</h3><p>Case studies for portfolio.</p>',
    ];
    
    $screen->add_help_tab( $args );
});

There is a lot you can do with help tabs in WordPress. Our example is just a basic setup to help you get moving in the right direction. You can learn about help tabs more in the WordPress codex.

By adding help tabs, you get to the heart of empowering the user and allowing them to feel like a hero.

Now, if you hang in there a little longer, we are about to move from the admin into the REST API, custom fields, and theme templating! But first, to do that, you need to understand post type hierarchy and those tricky little alert messages we promised to tackle.

Admin: Post Type Hierarchical

In WordPress, "Posts" can not have child posts but "Pages" can. With "Pages" a child page is a subpage. You can have your custom post type use hierarchy like "Pages" using the hierarchical argument. However, for the "Studies" example, we do not need this functionality.

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');
    
    // Declare what the post type supports
    $supports = ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'];

    $arguments = [
        'hierarchical' => false, // Do not use hierarchy 
        'supports' => $supports,
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'labels'  => $labels,
    ];
    register_post_type( $type, $arguments);
});

Admin: Messaging for Edit, Add, Bulk, and More.

content-image-make-post-type-example-messaging-1

If you are a true master of the details, you also noticed that your alert messages in the admin say "Post" and "Posts". To make alerts fit the post type you need to customize them.

When you update, add, edit, restore, draft, delete, or perform a single or bulk action on your custom post type WordPress displays alert messages. By default, a post type message for updating a post is "Post updated. View post.". The other messages will also say "Post" in some form.

Our "Study" post type alert post messages for single and bulk actions should not say "Post" but "Study". To update the messaging, we need two hooks:

  1. post_updated_messages - For single custom post type alert messages.
  2. bulk_post_updated_messages - For bulk custom post type alert messages.

Single Post Messages

The post_updated_messages allows us to add custom post messages in the admin for our post type.

add_filter( 'post_updated_messages', function($messages) {
    global $post, $post_ID;
    $link = esc_url( get_permalink($post_ID) );

    $messages['study'] = array(
        0 => '',
        1 => sprintf( __('Study updated. <a href="%s">View study</a>'), $link ),
        2 => __('Custom field updated.'),
        3 => __('Custom field deleted.'),
        4 => __('Study updated.'),
        5 => isset($_GET['revision']) ? sprintf( __('Study restored to revision from %s'), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
        6 => sprintf( __('Study published. <a href="%s">View study</a>'), $link ),
        7 => __('Study saved.'),
        8 => sprintf( __('Study submitted. <a target="_blank" href="%s" rel="noopener noreferrer">Preview study</a>'), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
        9 => sprintf( __('Study scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s" rel="noopener noreferrer">Preview study</a>'), date_i18n( __( 'M j, Y @ G:i' ), strtotime( $post->post_date ) ), $link ),
        10 => sprintf( __('Study draft updated. <a target="_blank" href="%s" rel="noopener noreferrer">Preview study</a>'), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
    );
    return $messages;
});

Bulk Post Messages

To add custom bulk messages in the admin for our post type we need to use the bulk_post_updated_messages hook.

add_filter( 'bulk_post_updated_messages', function( $bulk_messages, $bulk_counts ) {
    $bulk_messages['study'] = array(
        'updated'   => _n( "%s study updated.", "%s studies updated.", $bulk_counts["updated"] ),
        'locked'    => _n( "%s study not updated, somebody is editing it.", "%s studies not updated, somebody is editing them.", $bulk_counts["locked"] ),
        'deleted'   => _n( "%s study permanently deleted.", "%s studies permanently deleted.", $bulk_counts["deleted"] ),
        'trashed'   => _n( "%s study moved to the Trash.", "%s studies moved to the Trash.", $bulk_counts["trashed"] ),
        'untrashed' => _n( "%s study restored from the Trash.", "%s studies restored from the Trash.", $bulk_counts["untrashed"] ),
    );

    return $bulk_messages;
}, 10, 2 );

Let's move out of the admin design for now and onto the REST API. Great design is not just how things look; it's also about solving problems, and the REST API empowers problem-solving.

Enable the REST API for the Study Post Type ⚡️

The REST API is the next big thing in WordPress. You will also want your post type to take advantage of the newest parts of WordPress, like Gutenberg.

To enable the REST API for your custom post type it's as easy as setting the show_in_rest argument to true.

When you enable the REST API your post type will also start using Gutenberg if it also supports the editor.

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    // Declare what the post type supports
    $supports = ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'];

    $arguments = [
        'show_in_rest' => true, // Enable the REST API
        'hierarchical' => false,
        'supports' => $supports,
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'labels'  => $labels,
    ];
    register_post_type( $type, $arguments);
});

Enabling the WordPress REST API allows you to access your post type through the new WordPress endpoints as a JSON object. To view the post type REST endpoint go to the URL /wp-json/wp/v2/study.

If you would like to change the post type base name in the URL from study to studies use the rest_base argument.

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    // Declare what the post type supports
    $supports = ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'];

    $arguments = [
        'rest_base' => 'studies', // Change the REST base 
        'show_in_rest' => true,
        'hierarchical' => false,
        'supports' => $supports,
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'labels'  => $labels,
    ];
    register_post_type( $type, $arguments);
});

Boom! Like a pro. The API is good to go, and Gutenberg is enabled.

There is a lot you can do with the REST API and post types, but for us, its good just to know it's enabled.

Add Gutenberg to a Custom Post Type

To enable Gutenberg for a custom post type it must support the editor and have the REST API enabled.

Let's step back from the study post type for a moment to create a new post type named article . We will use the article post type to focus on enabling Gutenberg.

add_action( 'init', function() {
    $type = 'article';
    $labels = xcompile_post_type_labels('Article', 'Articles');

    $arguments = [
        'rest_base' => 'articles',
        'show_in_rest' => true, // Required for Gutenberg
        'supports' => ['editor'], // Required for Gutenberg
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'labels'  => $labels,
    ];
    register_post_type( $type, $arguments);
});

As you can see the two arguments we need to add Gutenberg are the following:

'show_in_rest' => true,
'supports' => ['editor'],

Removing Gutenberg from a Post Type

To remove Gutenberg from a post type, add the WordPress action hook use_block_editor_for_post_type to your theme's functions.php file with the following code.

add_filter('use_block_editor_for_post_type', function($enabled, $post_type) {

    // List of post types to remove
    $remove_gutenberg_from = ['study'];
    
    if (in_array($post_type, $remove_gutenberg_from)) { 
        return false; 
    }
    
    return $enabled;
}, 10, 2);

The above code will forcibly disable Gutenberg when your post type. Forcibly disabling Gutenberg is super handy when your post type also needs to be accessible from the WordPress REST API.

Not all post types should have Gutenberg enabled. Sometimes you want to use the classic editor with your custom post type. In those situations, this solution will give you the result you need.

Adding a Meta Box

content-image-make-post-type-meta-box-1

A meta box is a drag-and-drop element or container that appears on the dashboard or within a post type page. Let's add a meta box when registering our post type. This will give us a place to add our own custom input fields which we will do next. These custom fields are important because they let us break content into smaller parts giving us greater control of our design. Greater control is a great feeling.

To step toward adding a meta box to a post type we need to use the register_meta_box_cb argument.

Now, it is important to understand a few things when it comes to meta boxes:

  1. register_meta_box_cb does not add a meta box to the WordPress post type. It simply hooks us into the location WordPress will want us to add a meta box.... I know super confusing. Keep moving along and you will get it soon enough.
  2. We need to create a function to use with register_meta_box_cb. We will call it study_meta_box().
  3. To add a meta box we actually need to use the function add_meta_box. We can do that within the study_meta_box() function we are about to create.
  4. Again, register_meta_box_cb only allows us to hook into WordPress at the right place and time to register a meta box but does not add a meta box to the post type itself.
// The function for register_meta_box_cb
function study_meta_box(WP_Post $post) {
    add_meta_box('study_meta', 'Study Details', function() {
        echo 'Working';
    });
}

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    // Declare what the post type supports
    $supports = ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'];

    $arguments = [
        'register_meta_box_cb' => 'study_meta_box', // Register a meta box
        'rewrite' => [ 'slug' => 'studies' ]
        'has_archive' => true,
        'rest_base' => 'studies',
        'show_in_rest' => true,
        'hierarchical' => false,
        'supports' => $supports,
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'labels'  => $labels,
    ];
    register_post_type( $type, $arguments);
});

Great work! Adding a meta box is not so simple to understand but you did it. Now we can build the most powerful feature yet: Custom fields 🎉

Adding Custom Fields and Saving Them

Custom fields make it easy to take more control over the front-end design of your site. After all, some types of content need more than the "Title", "Featured Image" and "Editor". For example, what if a case study page needs a link to the client's website?

Sure you could simply add a link to the body copy of the case study. However, sometimes you need more flexibility. You don't always want the editor to control the full design. You need to break the different pieces of content into separate input fields.

This is why we use the meta box on the post types. We can use the meta box as a place to add the HTML we need for our custom input fields. Doing this will give us the control and flexibility we need.

Once you have a meta box and field are in place you need to save the field's value to the database. For a post type, the place to save your data is the post meta table, wp_postmeta, in the WordPress database.

Let's take a deep dive into writing an HTML input field and saving it to the WordPress database. Are you ready?

Adding the custom text field to our meta box

To add the custom input field with custom code we just need to visit our study_meta_box() function below and update the code that is there. In our case, we will create a simple HTML text input field named your_field. We will not get into other types of fields but the text field is very powerful.

Here are the things we need to consider for our text input field:

  1. Make sure we follow the WordPress design guidelines. For the sake of brevity, our code will cover the bare minimum.
  2. Consider accessibility and use <label>.
  3. Populate the field if there is an existing value using get_post_meta.
  4. Sanitize the field output with esc_attr. We can not always trust user input.
  5. Secure your meta box and all fields using wp_nonce_field.
// Add form and basic text field
function study_meta_box(WP_Post $post) {
    add_meta_box('study_meta', 'Study Details', function() use ($post) {
        $field_name = 'your_field';
        $field_value = get_post_meta($post->ID, $field_name, true);
        wp_nonce_field('study_nonce', 'study_nonce');
        ?>
        <table class="form-table">
            <tr>
                <th> <label for="<?php echo $field_name; ?>">Your Field</label></th>
                <td>
                    <input id="<?php echo $field_name; ?>"
                           name="<?php echo $field_name; ?>"
                           type="text"
                           value="<?php echo esc_attr($field_value); ?>"
                    />
                </td>
            </tr>
        </table>
        <?php
    });
}

Great Work! Now let's save the field when we update, save a draft, or publish our post type content.

Saving the Custom HTML field using WordPress and PHP

To save our custom field to WordPress we need to use the WordPress hook save_post. The save_post hook will be called when a post is added or updated. Because the hook fires when content is saved and updated it is a perfect place to wire our code to save custom fields.

When saving custom fields we need to consider a few things:

  1. Since post meta does not work with WordPress revisions we need to make sure our fields are not overridden when a post is autosave or restored.
  2. If our field is not present in the $_POST PHP super global we need to back out of saving since the save_post hook could be fired from anywhere and we do not want to delete our custom data if that happens.
  3. Make sure we complete our security check of the nonce field using check_admin_referer.
  4. We need to clean up our text field so we don't get empty strings but we do want to allow for the text value of 0.
  5. We need to store the field in the WordPress database wp_postsmeta. We can do this with update_post_meta. update_post_meta will not only update existing post meta but also add it if it does not exist.
  6. We need to delete the old metadata if there is no value in the input field to keep the database clean. This will be better than saving the meta value as an empty string.

With all this in mind, we can code the saving bits of our custom field.

// Check for empty string allowing for a value of `0`
function empty_str( $str ) {
    return ! isset( $str ) || $str === "";
}

// Save and delete meta but not when restoring a revision
add_action('save_post', function($post_id){
    $post = get_post($post_id);
    $is_revision = wp_is_post_revision($post_id);
    $field_name = 'your_field';

    // Do not save meta for a revision or on autosave
    if ( $post->post_type != 'study' || $is_revision )
        return;

    // Do not save meta if fields are not present,
    // like during a restore.
    if( !isset($_POST[$field_name]) )
        return;

    // Secure with nonce field check
    if( ! check_admin_referer('study_nonce', 'study_nonce') )
        return;

    // Clean up data
    $field_value = trim($_POST[$field_name]);

    // Do the saving and deleting
    if( ! empty_str( $field_value ) ) {
        update_post_meta($post_id, $field_name, $field_value);
    } elseif( empty_str( $field_value ) ) {
        delete_post_meta($post_id, $field_name);
    }
});

Now, this is exciting. Your admin design is getting very powerful and looking sharp. Yes, you can do a little dance. 🙌

Creating and saving custom fields is a big deal. If you want to add multiple fields you can modify what we have written. However, if you plan to code a lot of custom fields installing and using TypeRocket would be a faster way to code them.

TypeRocket lets you add more than just text fields. You can add gallery, radio, and repeater fields. Plus, it is 100% free!

Adding a Taxonomy

Taxonomies enable you to link related content. In WordPress "Posts" have the taxonomies "Tags" and "Categories". "Tags" let you link related posts to one another as well as label them so you can understand what the topics of the posts might include.

For our custom post type "Study" we can take advantage of "Tags" using the taxonomies argument.

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    $arguments = [
        'taxonomies' => ['post_tag'], // And post tags
        'register_meta_box_cb' => 'study_meta_box',
        'labels'  => $labels,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'public' => true,
        'has_archive' => true,
        'hierarchical' => false,
        'show_in_rest' => true,
        'rest_base' => 'studies',
        'supports' => ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'],
        'rewrite' => [ 'slug' => 'studies' ]
    ];

    register_post_type( $type, $arguments);
});

By default, WordPress has two taxonomies: "Tags" (post_tag) and "Categories" (category). You can also create your own taxonomies and use them for your post type. Much like post types, you need to register custom taxonomies. For taxonomies, use the function register_taxonomy.

Setting Up For The Front-end By Enabling Archives

At long last, we have arrived at the front-end design! To set-up the list of your custom post type content on the front-end you need to do three things:

  1. Enable the has_archive argument.
  2. Set the rewrite rules slug to the plural form of the post types name. In our case, this is "studies".
  3. Then flush the WordPress permlinks
add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    // Declare what the post type supports
    $supports = ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'];

    $arguments = [
        'rewrite' => [ 'slug' => 'studies' ] // Change the archive page URL
        'has_archive' => true, // Enable archive page
        'rest_base' => 'studies',
        'show_in_rest' => true,
        'hierarchical' => false,
        'supports' => $supports,
        'public' => true,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'labels'  => $labels,
    ];
    register_post_type( $type, $arguments);
});

Now that everything is in place you can start theming your custom post type content. For me, that is the default WordPress theme. For you that can be any theme!

Templating a Post Type In Your WordPress Theme

Templating a custom post type requires a little understanding of the WordPress Templating Hierarchy. If you are not familiar with how WordPress works with templates they use a pattern similar to how CSS handles specificity. However, we do not need to get bogged down with details this late in the game.

From the templating hierarchy, there are two template files we care about for the "Study" post type:

  1. single-study.php - For the single page when you go to http://example.com/studies/your-study-post to see a study.
  2. archive-study.php - For the archive page when you go to http://example.com/studies/ to see the list of studies.

By default, WordPress will use your themes single.php and archive.php templates for a custom post type's front-end design. When your specific post types template files exist, single-study.php and archive-study.php, they override the WordPress defaults.

You ready to start theming? Read on!

Single Page

content-image-make-post-type-example-template-1

Because we are using the WordPress Twenty Seventeen theme in this tutorial we need to create a new file name single-study.php and work with that file.

Within our single template file for the post type, we need to add a WordPress loop.

<?php
// single-study.php
get_header(); ?>

<div class="wrap">
    <div id="primary" class="content-area">
        <main id="main" class="site-main" role="main">
            <?php  while ( have_posts() ) : the_post(); ?>
                <h1><?php the_title(); ?></h1>
                <?php the_content(); ?>
            <?php endwhile; ?>
        </main>
    </div>
    <?php get_sidebar(); ?>
</div>

<?php get_footer();

Also, since we added a custom field to the post type meta box we can access it from within the loop as well.

<?php  while ( have_posts() ) : the_post(); ?>
    <h1><?php the_title(); ?></h1>
    <h4>
        <?php
        // Your custom field!
        $field_value = get_post_meta(get_the_ID(), 'your_field', true);
        echo esc_html($field_value);
        ?>
    </h4>
    <?php the_content(); ?>
<?php endwhile; ?>

Archive Page

For the post type's archive page we use the same process as we did for the single page. Only this time we use the archive-study.php template.

For the archive page, we will want to link to each study we look through and only show the title of the "Study". However, nothing is stopping you from having your own fun.

<?php
// archive-study.php
get_header(); ?>
    <div class="wrap">
        <header class="page-header">
            <?php the_archive_title( '<h1 class="page-title">', '</h1>' ); ?>
        </header>

        <div id="primary" class="content-area">
            <main id="main" class="site-main" role="main">
                <?php  while ( have_posts() ) : the_post(); ?>
                    <h1>
                        <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
                    </h1>
                <?php endwhile; ?>
            </main>
        </div>
        <?php get_sidebar(); ?>
    </div>
<?php get_footer();

With the archive page in place, everything is looking great... or is it? "What happens if you have 100 case studies?" 😱

Modifying the Archive Page Query

When you have an archive page it will only list as many items as your admin has specified in the "Settings > Reading" area. Many times you will not want the main archive page for a custom post type to be limited to the same number of items as your main blog feed. To remove the number limit on the archive page modify the main query using the hook pre_get_posts.

Once you are hooked into pre_get_posts you can access the main WP_Query object and modify it. To list every "Study" we need to:

  1. Hook into pre_get_posts.
  2. Detect if we are running the main query using the method is_main_query.
  3. Detect if we are on the "Study" archive page using is_post_type_archive.
  4. Set the posts_per_page of the main WP_Query and set it to -1. -1 will remove the number limit and list everything.
add_action('pre_get_posts', function( WP_Query $query ) {
    if($query->is_main_query() && $query->is_post_type_archive('study')) {
        $query->set('posts_per_page', -1);
    }
});

When you use the hook pre_get_posts you are accessing the SQL query that WordPress runs for you. This query is what gives you access to your custom post type, heck even "Posts" and "Pages", without having to write any SQL. That is kinda nice. 😎

Well, there is not much left to share. However, if you want to be a complete post type master you must learn one more thing. How to take total control!

Controlling Post Type Rights and Capabilities

When you create a post type anyone who can edit or add a "Post" in the admin and do so for your new post type. Most of the time this is what you want. However, some types of content you might want to restrict so that only an administrator can edit and add them.

Lockdown! 🙅‍♂️

When you need to add restrictions to your post type WordPress roles and capabilities come into play.

WordPress capabilities are the levels used to the manage permissions of authenticated users. For example, only an "Administrator" will have the capability to activate plugins by default. For an "Editor" to be able to activate plugins we need to add the right capability so they can.

Activate a Capability

To activate a capability for a WordPress "Role" like "Editor" you need to use the WP_Role object method add_cap. If you want to remove a capability you can use remove_cap.

When you update a role and add a capability it changes the database. If you do not want the database to be updated on every page load, and you do not, you need code only run on theme activation.

To run code when a theme is activated you need to use the hook after_switch_theme.

add_action( 'after_switch_theme', function() {
    $role = get_role( 'editor' );
    $role->add_cap( 'activate_plugins' );
});

Also, when your theme is deactivated you will want to remove any changes you made roles and capabilities. You can use the hook switch_theme to run code on your themes deactivation.

add_action('switch_theme', function() {
    $role = get_role( 'editor' );
    $role->remove_cap( 'activate_plugins' );
}));

Update Post Type Capabilities

Like we talk about before, every custom post type has the capabilities of "Posts". This means that your custom post type will have "Post" capabilities controlling who can use the new post type. The default post type capabilities will be:

  • edit_post
  • read_post
  • delete_post
  • edit_posts
  • edit_others_posts
  • publish_posts
  • read_private_posts
  • read
  • delete_posts
  • delete_private_posts
  • delete_published_posts
  • delete_others_posts
  • edit_private_posts
  • edit_published_posts
  • create_posts

However, you can remap, or rename, these capabilities and apply them to the "Role" you want the post type to be accessible. For us, that is the "Administrator" role. Doing this will remove the default permissions levels.

To set the capabilities we need to use the capabilities argument when we register our "Study" post type. Like when setting labels we will create a function to keep everything clean.

function compile_post_type_capabilities($singular = 'post', $plural = 'posts') {
    return [
        'edit_post'		 => "edit_$singular",
        'read_post'		 => "read_$singular",
        'delete_post'		 => "delete_$singular",
        'edit_posts'		 => "edit_$plural",
        'edit_others_posts'	 => "edit_others_$plural",
        'publish_posts'		 => "publish_$plural",
        'read_private_posts'	 => "read_private_$plural",
        'read'                   => "read",
        'delete_posts'           => "delete_$plural",
        'delete_private_posts'   => "delete_private_$plural",
        'delete_published_posts' => "delete_published_$plural",
        'delete_others_posts'    => "delete_others_$plural",
        'edit_private_posts'     => "edit_private_$plural",
        'edit_published_posts'   => "edit_published_$plural",
        'create_posts'           => "edit_$plural",
    ];
}

Next, we just need to apply the capabilities to our post type when it is registered.

add_action( 'init', function() {
    $type = 'study';
    $labels = xcompile_post_type_labels('Study', 'Studies');

    // Compile capabiltites
    $capabilities = compile_post_type_capabilities('study', 'studies');

    $arguments = [
        'capabilities' => $capabilities, // Apply capabilities
        'taxonomies' => ['post_tag'],
        'register_meta_box_cb' => 'study_meta_box',
        'labels'  => $labels,
        'description' => 'Case studies for portfolio.',
        'menu_icon' => 'dashicons-desktop',
        'public' => true,
        'has_archive' => true,
        'hierarchical' => false,
        'show_in_rest' => true,
        'rest_base' => 'studies',
        'supports' => ['title', 'editor', 'revisions', 'page-attributes', 'thumbnail'],
        'rewrite' => [ 'slug' => 'studies' ]
    ];

    register_post_type( $type, $arguments);
});

Finally, we must update the "Administrator" role's capabilities when the theme is activated and deactivated. Note, you will need to reactivate your theme if it is activated already.

add_action('after_switch_theme', function() {
    $role = get_role( 'administrator' );
    $capabilities = compile_post_type_capabilities('study', 'studies');
    foreach ($capabilities as $capability) {
        $role->add_cap( $capability );
    }
});

add_action('switch_theme', function() {
    $role = get_role( 'administrator' );
    $capabilities = compile_post_type_capabilities('study', 'studies');
    foreach ($capabilities as $capability) {
        $role->remove_cap( $capability );
    }
});

Controlling Visibility

The last piece in taking control of your post type is understanding its visibility. That is, whether your post type should be seen on the front-end and in the admin.

Think back to the beginning when we first register our post type. We did not mention it at the time but there is a public argument.

add_action( 'init', function() {
    $type = 'study';
    $label = 'Studies';
    $arguments = [
        'public' => true, // Allow access to post type
        'description' => 'Case studies for portfolio.',
        'label'  => $label
    ];
    register_post_type( $type, $arguments);
});

The public argument lets WordPress know if your post type should be visible in the admin and on the front-end of your site. If you set this setting to false the post type will not be seen on the front-end or the admin.

But, what happens if you want a post type for adding private "Notes" that should be visible in the admin but not visible on the front-end?

To handle situations like this WordPress gives you finer control over visibility. Let's take the "Notes" problem and solve it with a few more advanced visibility arguments:

  • show_in_nav_menus
  • show_in_admin_bar
  • show_in_menu
  • show_ui

By setting all of the above to true and the public setting to false we can make the "Notes" post type only visible in the admin.

add_action( 'init', function() {
    $type = 'notes';
    $labels = compile_post_type_labels('Note', 'Notes');
    $arguments = [
        'public' => false,
        'show_in_nav_menus' => true,
        'show_in_admin_bar' => true,
        'show_in_menu' => true,
        'show_ui' => true,
        'menu_icon' => 'dashicons-welcome-write-blog',
        'description' => 'Private notes.',
        'labels'  => $labels
    ];
    register_post_type( $type, $arguments);
});

When the "Notes" are accessible on the admin only and you publish a note it will no longer display a link under the title.

content-image-make-post-type-example-notes-1

Color me impressed that you are now a WordPress post type expert. You know just about everything there is to know of importance about post types. And with all of this, I leave you with a finishing thought.

The One Thing You Never Want to Forget

Remember the goal at the heart of crafting a post type is creating a user experience that is beautiful and clear. You won't need a post type for everything but you will need a heart that cares deeply about creating the best experience. An experience that leaves a user who has completed a task feeling like a hero.

You rock! ❤️ ❤️ ❤️

Extra Notes

  1. Normally, WordPress recommends making a plugin when registering post types. Don't worry! You don't need to learn to create a plugin. A plugin is only helpful because when you code a post type into a theme that post type and all the content into will be hidden if you switch or change themes. When you switch themes the content will not be lost or deleted. Do not worry there. Only, it will not be accessible.

  2. Flush the WordPress permlinks once you finish making a change to a post type.

  3. To customize the meta box further go to the WordPress documentation on adding a meta box.

  4. For more label options check out the documentation for register_post_type.

  5. For even more information on post types check the documentation.

Access More TypeRocket

Join our community mailing list and get notified before anyone else. Get videos and more.

By using our website you agree to our Cookie policy.