Customize with Custom Post Types

Customize with Custom Post Types

Online by JSC0d3
July 17, 2017 | | | | 1371 recognitions

One of the reasons WordPress powers 27% of the Internet is that it’s an incredibly flexible content management system. We all know about the standard Pages and Blog Posts that you can create easily with WordPress, but what about when you want something more customized and specific to your businesses’ needs? That’s where WordPress Custom Post Types and Custom Fields come in.

Customize with Custom Post Types

It’s pretty much universally agreed now that WordPress is more than just a blogging platform: It’s a Content Management System.

But in my view what makes it a powerful CMS is the ability to create custom content and display that content in exactly the way you need to. In this, the first of  of two posts I’ll look at the three types of custom content in WordPress and show you how to create that content. I’ll cover:

In this first part of a two-part post series, I’ll introduce the concept of custom content in WordPress and demonstrate how to register and make use of a custom post type, as well as exploring how WordPress displays posts you create using that custom post type.

Understanding and Using Each Kind of Custom Content

Before you start creating custom content, it’s useful to know exactly what each one does and what scenarios you might use them in. Let’s start with custom post types.

Custom Post Types

WordPress comes with a set of post types available to you by default. These are:

  • Posts
  • Pages
  • Attachments
  • Navigation menu items
  • Revisions
  • Links (if you’re using the blogroll feature)

You can also create your own custom post types to store content which doesn’t fit into any of the above categories. Examples might include products in an e-commerce site or events in a listings site. If you’ve ever used a plugin to create a store, event listing or any other kind of unique content, the chances are you’ve created custom post types without even knowing it.

A custom post type is nothing more than a regular post with a different post_type value in the database. The post type of regular posts is post, pages use page, attachments use attachment and so on. You can now create your own to indicate the type of content created. You could create custom post types for books, movies, reviews, products and so on.

If created correctly, you can achieve the following with a few lines of code:

  • The custom post type will show up in the back end as a separate menu item with its own post list and “add new” page
  • Navigating to will take you to the archive page for the post type. This is akin to visiting the front page for latest posts from the “post” post type.
  • Categories and tags can be made available to the custom post type, or you can create custom taxonomies.

Apart from these, you can modify countless options, such as where the custom post type should be placed in the menu, should it be searchable, which user level can access it, should it be hierarchical, custom rewrite rules, etc.

Different types of content have different data requirements. For regular posts, you’ll want to specify the author, category, date and so on. For a “book” custom post type, ideally you’d like to have the option to specify the book’s author, the page count, genre, publisher and other book-specific data. Using custom meta boxes, this is easily achieved and managed as well.

Custom meta boxes enable you to add additional boxes to the edit screen of a post. They usually use custom fields, so you could just use custom fields as well, but by separating out some custom fields as meta boxes, you can create a much smoother and usable admin.

Custom Post Type Definition

WordPress can hold and display many different types of content. A single item of such content is generally called a post, although a post is also a specific post type.

Internally, all the post types are stored in the same place, in the wp_posts database table, but are differentiated by a column called post_type.

Post type refers to the various structured data grouped together that is maintained in the WordPress database posts table.

Examples of post types are post (a group of blog posts), page (a group of pages), attachment (a group of uploaded media files), and revision (a groups of post revisions) that are native or built-in to WordPress.

Armed with the definition of post type, a new post type that is created and registered to WordPress is referred to as a custom post type.

If you’re building a company or business website with WordPress, examples of post types you could create are Portfolio, Testimonials and Products.

Now that we’ve understood the concept of custom post types, up next is learning how to create them.

It’s important to remember that custom post types are not posts. They don’t have any direct relation to posts and you should think of them separately. You can set up (or register) your custom post types to behave like posts or to behave like pages: it’s up to you. If they’re set up like posts you’ll be able to display archive pages of them while if they’re like pages they’ll be hierarchical. Most custom post types are configured to behave like posts but that doesn’t mean yours have to be.

Custom Taxonomies

Like custom post types, custom taxonomies add to the taxonomies that are already provided with WordPress. These are:

  • Categories
  • Tags
  • Post formats
  • Link categories (which apply to links in the blogroll if you’re using it)

You can also create your own custom taxonomies to organize your content more effectively. It’s important to remember that a custom taxonomy is not a category and has no relationship to categories: instead, ‘category’ is just one of the taxonomies used by WordPress.

Sometimes using the inbuilt categories will give you everything you need to sort your content, for example if you need to introduce a hierarchical category structure. But sometimes this won’t be enough, especially if you want to be able to display posts using more than one taxonomy, or if you’re using a custom post type and want to use a separate taxonomy for it.

Before deciding if you need to register a custom taxonomy, you need to ask yourself if the existing category system will do what you need. If all you need is to be able to add a hierarchy to your categories, you can already do this.

So if I was developing a training site (for example) and wanted to use categories to sort posts by the skill or knowledge being taught, I might use hierarchical categories. You can see here that I’ve got three top level categories: management development, personal development and technical skills, with everything else beneath these at the next level down.

But what if things got a bit more complicated? Let’s say I wanted to add a series of posts on communicating with your team.

It would make things easier if I could separate out the fact that these posts are for managers but also deal with communication skills. So maybe I could add a taxonomy for the audience, and use this to identify posts for managers instead of having management development as a category, I could create a new taxonomy for audiences. That way I can identify multiple topics and multiple audience groups for each post.

I’ll show you how to create a taxonomy like this in the next part of this series: once you’ve done it, you’ll be able to choose from both categories and audience groups in the post editing screen.

Coding Your Post Type

Creating a custom post type is pretty easy. Firstly, register the post type with the register_post_type() function and finally, wrap register_post_type() in a function call and hook it to the init Action like so:

function js_portfolio_cpt() {

    $args = array(
        'label'  => 'Portfolio',
        'public' => true,

    register_post_type( 'portfolio', $args );

add_action( 'init', 'js_portfolio_cpt' );

From the code above, you can see register_post_type() has a second function parameter that accepts a number of array arguments necessary in customizing every aspect of a custom post type.

If a CPT is created in this manner, it won’t show up in the admin dashboard menu (albeit still accessible via direct URL access ““) and its UI wordings (otherwise referred to as labels) and admin notices will be the same as the built-in post post type.

Let’s go over some of the array arguments for customizing CPTs and their respective functions.

function js_custom_post_product() {
  $labels = array(
    'name'               => _x( 'Products', 'post type general name' ),
    'singular_name'      => _x( 'Product', 'post type singular name' ),
    'add_new'            => _x( 'Add New', 'book' ),
    'add_new_item'       => __( 'Add New Product' ),
    'edit_item'          => __( 'Edit Product' ),
    'new_item'           => __( 'New Product' ),
    'all_items'          => __( 'All Products' ),
    'view_item'          => __( 'View Product' ),
    'search_items'       => __( 'Search Products' ),
    'not_found'          => __( 'No products found' ),
    'not_found_in_trash' => __( 'No products found in the Trash' ), 
    'parent_item_colon'  => '',
    'menu_name'          => 'Products'
  $args = array(
    'labels'        => $labels,
    'description'   => 'Holds our products and product specific data',
    'public'        => true,
    'menu_position' => 5,
    'supports'      => array( 'title', 'editor', 'thumbnail', 'excerpt', 'comments' ),
    'has_archive'   => true,
  register_post_type( 'product', $args ); 
add_action( 'init', 'js_custom_post_product' );


A plural descriptive name for your custom post type. For example, if you are creating a movie CPT, this should be Movies.

It will default to the value of $post_type which is the first parameter of register_post_type().


An array of labels for this post type. Each string is a bit of text displayed in a particular admin page.

Be sure to make these strings translatable if you’re creating a plugin for public use.

  • name: The plural form of the name of your post type.
  • singular_name: The singular form of the name of your post type.
  • add_new: The menu item for adding a new post.
  • add_new_item: The header shown when creating a new post.
  • edit_item: The header shown when editing a post.
  • new_item: Shown in the favorites menu in the admin header.
  • view_item: Shown alongside the permalink on the edit post screen.
  • search_items: Button text for the search box on the edit posts screen.
  • not_found: Text to display when no posts are found through search in the admin.
  • not_found_in_trash: Text to display when no posts are in the trash.

A full list of labels and their descriptions can be found here.


A short descriptive summary of what the post type is, although I haven’t found where this is used in WordPress admin.


Depending on its Boolean value, it’ll automatically decide what other arguments should be unless they’re specifically defined. If you’re looking to have more control over the public arguments, there are three specific arguments you may set:

  • show_ui: determines whether to show the administration screens.
  • publicly_queryable: determines whether queries for this post type can be performed from the front end.
  • exclude_from_search: whether the posts should appear in search results.

By default, a new post type is added after the ‘Comments’ menu item in the admin. But you have to ability to move it to a suitable position of your choosing.

For example, setting the menu_position value to 70 will add your menu item below Users.

New post types will default to the Posts menu icon, but if you want a custom icon in it instead, set this label to the URL of the icon or image file.

'menu_icon' => get_stylesheet_directory_uri() . '/images/portfolio-icon.png',

You can also use any dashicon as your CPT icon.

Say you want to use the download dashicon, set this label to the dashicon value as follows:

'menu_icon' => 'dashicons-download',


This argument allows you to decide whether to make your CPT hierarchical or not. The default value is false. Set to true to make your CPT hierarchical.


The supports argument allows you to define an array of meta boxes and fields that will appear on the screen when editing or creating a new post. This defaults to title and editor.

Setting this argument to false will prevent the default (title and editor) behavior.

There are several available options:

  • title: Text input field to create a post title.
  • editor: Content TinyMCE editor for writing.
  • author: A select box for changing the post author.
  • thumbnail: Featured image capability.
  • excerpt: A textarea for writing a custom excerpt.
  • trackbacks: Ability to turn trackbacks and pingbacks on/off.
  • custom-fields: Custom fields input field.
  • comments: Turn comments on/off.
  • revisions: Allows revisions to be made of your post.
  • post-formats: Add post formats, see the ‘Post Formats’ section
  • page-attributes: The attributes box shown for pages. This is important for hierarchical post types, so you can select the parent post.


Provides a callback function that will be called when setting up the meta boxes for the edit form. The callback function takes one argument $post, which contains the WP_Post object for the currently edited post.

This feature is particularly useful to developers for them to create custom meta boxes that will show up in the CPT edit screen.

'register_meta_box_cb' => 'metabox_callback_func',


An array of registered taxonomies like category or post_tag that will be used with this custom post type.

'taxonomies' => array( 'post_tag', 'category '),


Setting this argument to true will enable archives for your custom post type. For example, say your CPT is books, visiting will display a list of posts belonging to books custom post type.


This argument allows you to define the permalink structure of your custom post type when viewing a single post or archive.

Default value is true and uses $post_type as slug. To prevent rewrites, set to false.

Let’s see some examples for clarity sake.

Say you created a review custom post type but wish to change the URL slug from review to assessment, using the following rewrite argument will change the URL from to for single posts and to for the CPT archive.

'rewrite' => array(
    'slug'       => 'assessment',
    'with_front' => false

Whenever you do a rewrite of WordPress URL, ensure you click the Save Changes button in Settings >> Permalinks to recreate the rewrite rules.

So basically, the slug defines the new URL slug while with_front determines if the permalink structure be pre-pended with the front base.

If with_front is set to false, the URL of a single post and post archive becomes and respectively but if it is set to true, the URL of a single post and post archive respectively becomes and

Notice the omission of blog in the latter? That’s the difference.


Use this argument to decide whether posts belonging to your custom post type can be exportable via the WordPress export tool. By default, this is set to true.


This argument allows you to control the query variable used to get posts of this type.

If set to true, it will allow you to request a book custom posts type via where harry-potter that is the URL slug of a book entry or post.

If set to a string rather than true (for example publication), you can do:

Caveat of “query_var”

If query_var is undefined in your CPT registration array argument, it defaults to $post_type thus, it is always defined unless you set it to false.

Here comes the caveat. Whenever the value of a query_var is added as a query string to a URL, it will lead to a 404.

Let me clarify. Say the value of your CPT query_var is review and a query string with the key set to review was added to any URL of your WordPress site in any of the following forms:


This will cause a 404 to happen.

Custom Interaction Messages

WordPress generates a number of messages triggered by user actions. Updating, publishing, searching, etc., in the back end all lead to messages which — by default — are tailored to regular posts. You can change the text of these messages easily by using the post_updated_messages hook.

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

An associative array would be far better; we could see what each message is for without having to read the actual message.

Notice that you can change the messages for all custom post types using this single function. The $messages array holds the messages for all post types, so you can modify them all here. I personally create a function for each post type just so I can group the post type creation and the custom messages together easily.

Contextual Help

A feature I rarely see implemented is the customized contextual help. As a user, I’ve never actually used this feature myself, but I’m sure that many people do; in any case, it’s nice to provide some hand-holding for less experienced users.

The contextual help feature is a descending tab which can be seen in the top right of pages where available. Let’s take a look at how the contents can be changed.

function js_contextual_help( $contextual_help, $screen_id, $screen ) { 
  if ( 'product' == $screen->id ) {

    $contextual_help = '<h2>Products</h2>
    <p>Products show the details of the items that we sell on the website. You can see a list of them on this page in reverse chronological order - the latest one we added is first.</p> 
    <p>You can view/edit the details of each product by clicking on its name, or you can perform bulk actions using the dropdown menu and selecting multiple items.</p>';

  } elseif ( 'edit-product' == $screen->id ) {

    $contextual_help = '<h2>Editing products</h2>
    <p>This page allows you to view/modify product details. Please make sure to fill out the available boxes with the appropriate details (product image, price, brand) and <strong>not</strong> add these details to the product description.</p>';

  return $contextual_help;
add_action( 'contextual_help', 'js_contextual_help', 10, 3 );

This is also a bit difficult because you have to know the ID of the screen you are on. If you print out the contents of the $screen variable, you should be able to determine the ID easily. This is also a function you can use to modify the contextual help of all custom post types at once, but I personally recommend grouping this together with the previous two blocks and only using it for one custom post type at a time.


To quickly recap, we used three functions to create a “complete” custom post type. We used register_post_type() to create the post type itself and two hooks — contextual_help and post_updated_messages — to create helpful guidance and relevant messages respectively.

Post Meta Boxes

Meta boxes are the draggable boxes you see in the WordPress edit screen for a post. There are numerous built-in meta boxes like the publishing controls, the taxonomies, the author box, etc., but you can create some for yourself.

Meta boxes tend to be used to manage custom field data in a much more user-friendly way than the built-in custom fields box does. Since you put the controls in place, you can add client-side error checking and many other fancy things.

Justin Tadlock wrote the all-encompassing custom meta box article here on Smashing Magazine, which is a great in-depth article on the subject. I recommend reading it for the full picture, but I’ll get you started here.

Creating a meta box requires three steps:

  • Define the box itself,
  • Define the content of the meta box,
  • Define how the data from the box is handled.

Defining the Meta Box

add_action( 'add_meta_boxes', 'js_product_price_box' );
function js_product_price_box() {
        __( 'Product Price', 'textdomain' ),

The code above creates the meta box with the following parameters (in the order given):

  • The unique identifier for the meta box (it does not have to match the function name),
  • The title of the meta box (visible to users),
  • The function which will display the contents of the box,
  • The post type the meta box belongs to,
  • The placement of the meta box,
  • The priority of the meta box (determines “how high” it is placed).

Defining the Content of the Meta Box

function product_price_box_content( $post ) {
  wp_nonce_field( plugin_basename( __FILE__ ), 'product_price_box_content_nonce' );
  echo '<label for="product_price"></label>';
  echo '<input type="text" id="product_price" name="product_price" placeholder="enter a price" />';

This is a simple box which only contains the price, so we have created a label and an input to manage it. A nonce field is also present, which adds security to the data submission.

Handling Submitted Data

In most cases, you will want to save the data as a custom field, but you are by no means restricted to this method. You could use the input to make a third party API call, to generate an XML file or whatever you like. The most common use is saving custom post data, so let’s take a look at how that is done.

add_action( 'save_post', 'js_product_price_box_save' );
function js_product_price_box_save( $post_id ) {

  if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) 

  if ( !wp_verify_nonce( $_POST['product_price_box_content_nonce'], plugin_basename( __FILE__ ) ) )

  if ( 'page' == $_POST['post_type'] ) {
    if ( !current_user_can( 'edit_page', $post_id ) )
  } else {
    if ( !current_user_can( 'edit_post', $post_id ) )
  $product_price = $_POST['product_price'];
  update_post_meta( $post_id, 'product_price', $product_price );

The biggest part of this function is all about safety. First of all, if an autosave is being performed, nothing happens because the user hasn’t actually submitted the form. Then the nonce is checked, followed by permission checking. If these are all passed, we take our data and add it to the post using the update_post_meta() function.

Displaying Your Content

While there a plenty of nuances to all of the above, you should be familiar with the basics. All that is left is to actually use the data we now have and show things to the user. This involves showing posts — perhaps from various custom post types and taxonomies — and using our post metadata.

Displaying Posts

If you’ve created a post type with the has_archive parameter set to “true,” WordPress will list your posts on the post type’s archive page. If your post type is called “books,” you can simply go to and you’ll see your post list.

This page uses archive-[post_type].php for the display if it exists (archive-books.php in our case). If it doesn’t exist, it will use archive.php and if that doesn’t exist it will use index.php.

Another way to display custom post type content is to use a custom query with the WP_Query class. To display posts from a certain post type and custom taxonomy, you could do something like this:

$args = array(
      'post_type' => 'product',
      'tax_query' => array(
          'taxonomy' => 'product_category',
          'field' => 'slug',
          'terms' => 'boardgames'
    $products = new WP_Query( $args );
    if( $products->have_posts() ) {
      while( $products->have_posts() ) {
          <h1><?php the_title() ?></h1>
          <div class='content'>
            <?php the_content() ?>
    else {
      echo 'Oh ohm no products!';

Displaying Metadata

Metadata can be retrieved easily using the get_post_meta() function. In our example above, we saved a post meta field named product_price. We can retrieve the value of this field for a given post using the following code:

  // If we are in a loop we can get the post ID easily
  $price = get_post_meta( get_the_ID(), 'product_price', true );

  // To get the price of a random product we will need to know the ID
  $price = get_post_meta( $product_id, 'product_price', true );

Speeding up Custom Post Types Setup with Plugins

Now that we’ve covered the foundations, it’s a good time to point out that there are a number of plugins in WordPress plugin repository that makes creating custom post types very easy.

Examples include (but are not limited to):

A handy tool for generate taxonomy is: generatewp.

Wrap up

Being able to create custom content makes WordPress very powerful: you can uses it to add and display a range of flexible content types in whichever way you need to.

In this post you’ve learned about the three types of custom content, what they are and when to use each of them. You’ve also learned how to create a custom post type and display it on your site.

Do you use custom post types in your sites? Do you prefer to use a plugin or code your own? What do you find them most useful for? Let us know your thoughts in the comments.

JSC0d3's Logo
About JSC0d3

JSC0d3 is an entrepreneur, online marketer, and an employee of an IT company. When not building websites, creating content, or helping customers improve their online business, spend time with their wife and two beautiful children. Although he still feels new in WordPress, he enjoys sharing what he has learned with all of you! If you want to get in touch with him, you can do this through this website.

On the same idea

Posted by | February 9, 2019

How fast your website loads can have a direct effect on whether you turn visitors into new customers and fans When your site is delivered to...

Posted by | February 9, 2019

With today’s high-speed internet connections and advances in web development techniques, there is no reason that your website should be...

Posted by | February 9, 2019

WordPress' wp_link_pages() function, used for displaying page links in multi-page posts, lacks one big feature You can display a list of...

Previous PostBackNext Post
2 impressions on “Customize with Custom Post Types


Great idea! But should be more accurate, don’t think so…


I’ll try next time

Leave here an impression