WordPress has many strengths, but out of the box it’s somewhat limited in the type of content you can store and how it displays. Enter custom post types (CPTs). In this article, I’ll show you the hard way to create CPTs, look at why plugins may not be your best option, and finally, I’ll show you one of the easiest ways to create your own CPTs without a plugin.
WordPress, at its core, has always been a content management system and content creation platform. Custom post types can bring virtually endless content possibilities to the already powerful platform of WordPress.
WordPress comes with two main types of content: posts and pages. These are the types used most frequently, but default WordPress includes a few others:
These are sufficient for most purposes, but what happens if you want to build a WordPress site with different types of content?
That’s where the power of CPTs comes in. In short, CPTs provide you with the ability to create virtually any kind of post. When combined with the ability to display on custom templates, the options are almost limitless.
Remember, “with great power, comes great responsibility.” Custom post types put that power in your hands. It’s up to you to make sure you use it responsibly.
While CPTs provide the ability to expand your site or theme’s functionality, it comes at a cost. First, the weight and complexity of your site’s database increases with each new post type you add, all sharing the same table in the database. This can negatively impact performance as the table size grows. Second, having too many CPTs also adds needless complexity and confusion to your WordPress admin, not to mention a very large menu sidebar.
It’s agony to go through a client’s site, sift through different post types, and try to locate a specific piece of content that had no business being anything other than a regular post with a Post Format applied.
On that note, here are some rules of thumb you can use when deciding if you really need to create your own CPTs.
For many of us, plugins are the first place we turn to help save time. There are a number of options in the WordPress plugin repository, with Custom Post Type UI leading the pack in installations.
While plugins are a great option for many WordPress-specific scenarios, there are a few reasons why you may want to consider integrating CPTs programmatically instead.
If you’re building a plugin—particularly one you want to resell—it’s a good idea to make everything as pre-packaged as possible.
Requiring someone to buy and download your plugin, then download another plugin and configure it to specific settings is an annoying process. A lot of potential users will just look for another option that doesn’t require as much work.
CPTs are commonly used in plugins to display the type of content ordinary posts and pages can’t manage. For example, ecommerce plugin WooCommerce includes a CPT called products
, and WPForms has a CPT called wpforms
. Advanced Custom Fields itself uses a CPT for its Field Group data.
When you’re looking to incorporate CPTs into your theme or client site, there’s a pretty good chance that flexibility and customization are the main benefits you want to achieve.
With a plugin, you’re limited to the specific functionality the plugin offers. It may not have all the tools you need to get the job done.
A plugin adds overhead. The function that reads your stored CPT data from the database and then calls register_post_type
using that CPT data is built right into WordPress. This means additional database calls and PHP runtime, which increases as you add more and more CPTs with the plugin.
WordPress plugins are virtually inescapable. As such, there aren’t many WordPress users or developers who haven’t reached a point where they’ve installed one too many.
Tacking on one more for CPTs just adds one more item to manage, upkeep, and update as your site ages. Plus, if anything goes wrong with that plugin, you will suddenly lose whole sections of your site.
Once you’ve determined that a new custom post type is right for you, it’s time to make use of the register_post_type
WordPress function and configure it accordingly.
To start, here’s a set of sample code you can copy into a custom plugin for the site or an mu-plugin file, and edit for registering your custom post type. We don’t recommend using your theme’s functions.php
file or putting any site code in the theme that would be lost if the active theme is changed.
// Register Custom Post Type
function hfm_register_custom_post_type() {
$labels = array(
'name' => _x( 'Post Types', 'Post Type General Name', 'text_domain' ),
'singular_name' => _x( 'Post Type', 'Post Type Singular Name', 'text_domain' ),
'menu_name' => __( 'Post Types', 'text_domain' ),
'name_admin_bar' => __( 'Post Type', 'text_domain' ),
'archives' => __( 'Item Archives', 'text_domain' ),
'attributes' => __( 'Item Attributes', 'text_domain' ),
'parent_item_colon' => __( 'Parent Item:', 'text_domain' ),
'all_items' => __( 'All Items', 'text_domain' ),
'add_new_item' => __( 'Add New Item', 'text_domain' ),
'add_new' => __( 'Add New', 'text_domain' ),
'new_item' => __( 'New Item', 'text_domain' ),
'edit_item' => __( 'Edit Item', 'text_domain' ),
'update_item' => __( 'Update Item', 'text_domain' ),
'view_item' => __( 'View Item', 'text_domain' ),
'view_items' => __( 'View Items', 'text_domain' ),
'search_items' => __( 'Search Item', 'text_domain' ),
'not_found' => __( 'Not found', 'text_domain' ),
'not_found_in_trash' => __( 'Not found in Trash', 'text_domain' ),
'featured_image' => __( 'Featured Image', 'text_domain' ),
'set_featured_image' => __( 'Set featured image', 'text_domain' ),
'remove_featured_image' => __( 'Remove featured image', 'text_domain' ),
'use_featured_image' => __( 'Use as featured image', 'text_domain' ),
'insert_into_item' => __( 'Insert into item', 'text_domain' ),
'uploaded_to_this_item' => __( 'Uploaded to this item', 'text_domain' ),
'items_list' => __( 'Items list', 'text_domain' ),
'items_list_navigation' => __( 'Items list navigation', 'text_domain' ),
'filter_items_list' => __( 'Filter items list', 'text_domain' ),
);
$args = array(
'label' => __( 'Post Type', 'text_domain' ),
'description' => __( 'Post Type Description', 'text_domain' ),
'labels' => $labels,
'supports' => false,
'taxonomies' => array( 'category', 'post_tag' ),
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 5,
'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' => 'page',
);
register_post_type( 'post_type', $args );
}
add_action( 'init', 'hfm_register_custom_post_type' );
While copying and pasting the above code isn’t difficult, it also won’t get you very far. Your goal isn’t just to create custom posts, but custom content as well. To do that, we’ll need to fill in those arrays with the right args. Figuring those out and coding them all manually can be very time consuming.
Luckily, there’s a tool that makes it easier to create your custom post types (without a plugin). It’s called GenerateWP.
With GenerateWP, you’re provided a step-by-step wizard that helps you craft all the specifications of your CPT:
GenerateWP gives us 9 tabs we can use to adjust our new CPT. We’ll discuss some of these tabs in detail below.
The “General” tab is displayed above, and allows you to input the function name, whether it supports child themes, and whether to include a text domain for translation. For the function name, input whatever you’d like to call it, followed by _post_type
.
In the “Post Type” tab, you can set the “Post Type Key”, provide a description, and set the singular and plural names for the post type. You can also link to taxonomies on this tab.
Taxonomies are a great way to help categorize and organize content—particularly when we’re dealing with CPTs. As you can see, the function for creating your own taxonomy is similar to that of a new CPT:
// Register Custom Taxonomy
function hfm_custom_taxonomy() {
$labels = array(
'name' => _x( 'Taxonomies', 'Taxonomy General Name', 'text_domain' ),
'singular_name' => _x( 'Taxonomy', 'Taxonomy Singular Name', 'text_domain' ),
'menu_name' => __( 'Taxonomy', 'text_domain' ),
'all_items' => __( 'All Items', 'text_domain' ),
'parent_item' => __( 'Parent Item', 'text_domain' ),
'parent_item_colon' => __( 'Parent Item:', 'text_domain' ),
'new_item_name' => __( 'New Item Name', 'text_domain' ),
'add_new_item' => __( 'Add New Item', 'text_domain' ),
'edit_item' => __( 'Edit Item', 'text_domain' ),
'update_item' => __( 'Update Item', 'text_domain' ),
'view_item' => __( 'View Item', 'text_domain' ),
'separate_items_with_commas' => __( 'Separate items with commas', 'text_domain' ),
'add_or_remove_items' => __( 'Add or remove items', 'text_domain' ),
'choose_from_most_used' => __( 'Choose from the most used', 'text_domain' ),
'popular_items' => __( 'Popular Items', 'text_domain' ),
'search_items' => __( 'Search Items', 'text_domain' ),
'not_found' => __( 'Not Found', 'text_domain' ),
'no_terms' => __( 'No items', 'text_domain' ),
'items_list' => __( 'Items list', 'text_domain' ),
'items_list_navigation' => __( 'Items list navigation', 'text_domain' ),
);
$args = array(
'labels' => $labels,
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => true,
);
register_taxonomy( 'taxonomy', array( 'post' ), $args );
}
add_action( 'init', 'custom_taxonomy', 0 );
By default, GenerateWP will give your CPT the same categories and tags as the base post type of WordPress. However, in many cases, you won’t want to share taxonomies with posts, and you’ll want new taxonomies specific to your CPT. The field in the “Post Type” tab allows you to link to a taxonomy, but it won’t create one. You can create a new taxonomy using the function shown above, but this can be somewhat time consuming. To save time, you can use GenerateWP’s taxonomy tool instead.
This tab allows you to create an array that defines the labels for your new CPT. The CPT will inherit default labels for ones you don’t include.
The “Options” tab is where you indicate which elements of your new CPT are editable. “Title” and “Content (editor)” are selected by default, but you can toggle these off if you have a reason to create a CPT where the title and content cannot be changed.
This tab also gives you options related to searchability, exporting, and archives.
This has nothing to do with whether your CPT is visible on the frontend and everything to do with how it displays in your WordPress navigation and admin menus. You can set these options here, including whether or not the CPT has its own icon as a menu item.
This tab allows you to configure how your new CPT interacts with WP_Query
, a PHP class that WordPress primarily uses to pull posts from the database. For a thorough explanation of how WP_Query
works, check out this WP Engine article.
While you can change permalink settings in your WordPress admin, those changes only apply to blog posts. WordPress uses the default permalink structure for both custom post types and custom taxonomies.
This tab in GenerateWP allows you to alter the permalink settings for your new CPT. The “Permalink Rewrite” dropdown gives you three options:
You can typically leave permalink settings alone when creating a new CPT. Make sure you have a good reason before altering any of these settings.
This doesn’t have anything to do with the capabilities of your CPT. Rather, it allows you to set capabilities by Role. Changing Base capabilities to Custom capabilities unlocks the rest of the options, giving you the ability to fine tune what can be changed by which person.
This tab allows you to decide if the new CPT will be accessible via the REST API. In addition, you can change the base URL and the controller class name.
CPTs and custom taxonomies can use the same controllers as your default post types and taxonomies. However, it is possible to use your own controllers and namespace instead. Before changing any of these settings, it’s important to note that using the default controllers increases the chances of third-party compatibility. You’ll need to make sure to enable your CPT in the REST API if you want to use the Block Editor to edit content for your CPT.
Once you’ve configured all the settings for your custom post type, click Update Code and GenerateWP will format the code so you can simply copy and paste it directly into your plugin. You can also click Save Snippet and give your new CPT code a title and description. Premium users can also mark the snippet “Private.”
If you’re going through the trouble of creating a CPT and taxonomies, more than likely you also want to add custom fields not afforded by a standard post.
While GenerateWP offers a paid service for creating meta boxes meant for enabling custom fields for your CPT, we have another way.
Building custom meta fields is one of the most time consuming parts of creating WordPress sites. This is especially true if you want any type of drag and drop functionality available to the client or end user.
This is a case where a plugin is a no-brainer. Specifically, Advanced Custom Fields.
I won’t dive into a complete tutorial of the full capabilities of ACF here, but you can check out our Getting Started guide and field group doc for more information. Once your CPT has been registered in ACF, you can simply select it from a dropdown to incorporate virtually any field type in your new CPT:
We know that there’s a lot of complexity involved in custom WordPress development—particularly when it comes to CPTs.
My hope is that the tools and methods mentioned in this article will seriously reduce the time and complexity required to build your own WordPress themes and plugins.