23 Jun

A Super Simple Method for Creating Your Own WordPress Custom Post Types Without a Plugin

By Mike Davey

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). One of our most common user requests has been to add this capability to ACF. The release of ACF 6.1 allows you to register CPTs and custom taxonomies along with your custom fields, eliminating the need for a separate plugin. In some cases, though, you may want to generate CPTs without any plugins at all, such as when you’re developing custom plugins or themes. In this article, I’ll show you the hard way to create CPTs for use in your own plugins or themes, as well as one of the easiest ways you could imagine.

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.

What is a Custom Post Type?

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:

  • Attachments
  • Revisions
  • Navigation menus
  • Custom CSS
  • Changesets (revisions for the Customizer framework)

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.

When Not to Use Custom Post Types

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.

  • Would it work just as well as a Post Format? More than likely, you’re better off creating one of these instead.
  • Could the content you’re developing utilize custom fields within the base post type? Advanced Custom Fields makes this incredibly easy with little need for coding and a virtually endless supply of customization and functionality.
  • Are you using the standard post type at all? If you’re creating a CMS with very specific types of content, it may make sense to customize posts with Post Formats, taxonomies, and custom fields to fit your needs rather than needlessly multiplying post types.
  • Are you trying to leverage CPTs for data that really should be in a custom table?

Why Not Just Use a Plugin?

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.

Building Plugins

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.

Adding a new field group in ACF. ACF uses a custom post type to define field groups.

Lack of Flexibility

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.

Extra Bloat

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.

Added Plugin Management

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.

Creating Your First Custom Post Type

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:

The first stages of creating a custom post type with GenerateWP's wizard.

GenerateWP gives us 9 tabs we can use to adjust our new CPT. We’ll discuss some of these tabs in detail below.

  • General
  • Post Type
  • Labels
  • Options
  • Visibility
  • Query
  • Permalinks
  • Capabilities
  • Rest API

General Tab

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.

Post Type Tab

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.

Highlighting the Link to Taxonomies field on the Post Type tab on GenerateWP.

Integrating Custom Taxonomies

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.

Labels Tab

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 Labels tab in GenerateWP allows you to assign labels to your custom post type.

Options Tab

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.

The Options tab in GenerateWP allows you to decide which parts of the CPT will be editable.

Visibility Tab

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.

Backend visibility options in GenerateWP's CPT generator.

Query Tab

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.

Configuring how WP_Query will interact with a new CPT in GenerateWP.

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:

  • Default permalink, which uses the post type key.
  • No permalink, which prevents URL rewriting.
  • Custom permalink, which activates the other options shown here.

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.

GenerateWP allows you to alter the permalink structure of your custom post types.

Capabilities Tab

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.

The Capabilities tab in GenerateWP allows you to indicate who can edit the CPT.


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.

The REST API Tab in GenerateWP's custom post generator.

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.”

Add in Custom Fields

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:

Using Advanced Custom Fields, you can incorporate virtually any field type into your 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.

About the Author