Creating a Custom Post Type

Custom post types are a fantastic tool for anyone who uses WordPress. For a site owner, they allow you organize different types of content. This extends to customizing the way you present that content to the user. For a developer, they’re a great way to test your projects. Working on saving some metadata to the database in a novel way? Make a custom post type and tell WordPress to only apply your changes to that post type. Working with a client who uses a few different custom post types on their site? Replicate those post types in your local environment and rest easy knowing your code will translate.

Quick and Dirty Custom Post Type Recipe

Let’s say I want to make a no-frills custom post type for testing purposes. I don’t want to install a whole plugin to make this happen. Here’s a readymade function you can slap into the the functions.php of your current theme or the $plugin-name.php of your plugin to quickly get a new post type up and running.

function my_project_register_type($key, $plural, $singular){

	$labels = array(
	  'name'                  => $plural,
	  'singular_name'         => $singular,
	  'menu_name'             => $plural,
	  'name_admin_bar'        => $singular,
	  'add_new'               => __( 'Add New', "my-project" ),
	  'add_new_item'          => sprintf( __( 'Add New %1$s', "my-project" ), $singular),
	  'new_item'              => sprintf( __( 'New %1$s', "my-project" ), $singular),
	  'edit_item'             => sprintf( __( 'Edit %1$s', "my-project" ), $singular ),
	  'view_item'             => sprintf( __('View %1$s', "my-project" ), $singular),
	  'all_items'             => sprintf( __( 'All %1$s', "my-project" ), $plural),
	  'search_items'          => sprintf( __( 'Search %1$s', "my-project" ), $plural),
	  'parent_item_colon'     => sprintf( __('Parent %1$s:', "my-project" ), $plural),
	  'not_found'             => sprintf( __( 'No %1$s found.', "my-project" ), $plural),
	  'not_found_in_trash'    => sprintf( __( 'No %1$s found in Trash.', "my-project" ), $plural),
	  'archives'              => sprintf( __( '%1$s archives', "my-project" ), $singular),
	  'insert_into_item'      => sprintf( __( 'Insert into %1$s', "my-project" ), $singular ),
	  'uploaded_to_this_item' => sprintf( __( 'Uploaded to this %1$s', "my-project" ), $singular),
	  'filter_items_list'     => sprintf( __( 'Filter %1$s list', "my-project" ), $plural),
	  'items_list_navigation' => sprintf( __( '%1$s list navigation', "my-project" ), $plural),
      'items_list'            => sprintf( __( '%1$s list', "my-project" ), $plural),

	$args = array(
      'labels'             => $labels,
      'public'             => true,
      'show_in_rest'       => true,
      'has_archive'        => true,
      'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments', 'page-attributes', 'custom-fields' ),

	register_post_type( $key, $args );

All this function does is specify a few parameters to override the defaults of WordPress’ native register_post_type. Give it a post type key, a singular name, and a plural name, and in return it provides a whole new post type with features comparable to the default “post” post type. Let’s look at the specific parameters it passes to register_post_type. Here’s why I chose them:

  • labels: Labels are nice. They keep things clear and serene.
  • public: It’s kind of surprising that custom post types are not viewable on the front end by default, but they’re not. To change that, set this parameter to true.
  • show_in_rest: I really like the Gutenberg editor. You have to explicitly set this parameter to true in order to use it with your custom post type.
  • has_archive: Regular posts naturally have an archive page. It’s located on either the front page or the designated “posts page” set in homepage settings. Custom post types, on the other hand, don’t have an archive page by default. Setting this parameter to true will give them one at “”. Interestingly, even with this parameter set to false, there’s still a way to see an archive-like list of all the posts belonging to your custom post type as long as the publicly_queryable parameter has a value of true. The list will be located at the same url as the archive would be, namely “”. The difference is that it’s rendered by index.php rather than being eligible for archive-$posttype.php or archive.php.
  • supports: Might as well make our custom post type as fully-featured as possible.

Calling Our Function

Hook a function to the “init” action and call the register function inside of it. Let’s say you’re working on site for an organization that has sponsors. You want to make a post for each of them.

function my_project_register_post_types(){
  my_project_register_type( 'myproject_sponsors', __('Sponsors', 'my-project'), __('Sponsor', 'my-project') );
add_action('init', 'my_project_register_post_types' );

Remember that you may get some 404 errors when your post type is newly created. The solution for this is a quick rewrite flush.