Creating custom post type

Custom post type

Custom post types (CPT) are an essential part of WordPress. If you have a specific plugin installed, chances are you have custom post types set up as well. Let's go and see how can we create them on our own.

Custom post types are a useful tool if you want to create a new plugin, and you want to differentiate your posts from the rest of the native posts in the WordPress. The default post types in WordPress are: ‘post’, ‘page’, ‘attachment’, ‘revision’ and ‘nav_menu_item’. Yes, menu items are post types – sounds crazy, but it actually works pretty well.

If you’ve ever used a plugin like bbpress, WooCommerce, The Events Calendar etc. you used custom post types. Every plugin maker knows what they are and that you can do a bunch of cool stuff with them. So for today’s tutorial I’ve thought that it would be cool to show you guys how to create a custom post type, put in a meta box, and some additional fields to it and register a taxonomy for that custom post type.

Knowledge is power

The post type we’ll be creating is called: Knowledge base. I’ve been working on a site that required one, so with all the info still fresh, it’s best to use that knowledge to help you guys out :D

As always my base is plain old Twenty sixteen theme in the functions.phpfile, but I usually recommend adding this to a separate file you’ll include inside your functions file, to avoid the clutter. Or, better yet, you’ll add it in a plugin. First thing we’ll want to do is to register our CPT and our taxonomy (while we’re at it). The function that controls this is called register_post_type(). You can read more about it here.

/**
 * Custom post type and taxonomy registration
 *
 * @since		1.0.0
 */

add_action('init', 'knowledgebase_register_type');

if (!function_exists('knowledgebase_register_type')) {
	function knowledgebase_register_type() {

	    /**
		 * Post type label arguments
		 *
		 * @var     array     $labels     Argument array for custom post type labels
		 * @since		1.0.0
		 */
	    $labels = array(
			'name'               => esc_html_x( 'Knowledge base', 'post type general name', 'mytheme' ),
			'singular_name'      => esc_html_x( 'Knowledge base', 'post type singular name', 'mytheme' ),
			'menu_name'          => esc_html_x( 'Knowledge base', 'admin menu', 'mytheme' ),
			'name_admin_bar'     => esc_html_x( 'Knowledge base', 'add new on admin bar', 'mytheme' ),
			'add_new'            => esc_html_x( 'Add New Article', 'knowledgebase', 'mytheme' ),
			'add_new_item'       => esc_html__( 'Add New Article', 'mytheme' ),
			'new_item'           => esc_html__( 'New Knowledge base Article', 'mytheme' ),
			'edit_item'          => esc_html__( 'Edit Knowledge base Article', 'mytheme' ),
			'view_item'          => esc_html__( 'View Knowledge base Article', 'mytheme' ),
			'all_items'          => esc_html__( 'All Knowledge base Articles', 'mytheme' ),
			'search_items'       => esc_html__( 'Search Knowledge base', 'mytheme' ),
			'parent_item_colon'  => esc_html__( 'Parent Knowledge base Article:', 'mytheme' ),
			'not_found'          => esc_html__( 'No Article.', 'mytheme' ),
			'not_found_in_trash' => esc_html__( 'No articles found in Trash.', 'mytheme' )
		);

	    $args = array(
	        'label'                => esc_html__('Knowledgebase', 'mytheme'),
	        'labels'               => $labels,
	        'singular_label'       => esc_html__('Knowledgebase', 'mytheme'),
	        'menu_icon'            => 'dashicons-book',
	        'public'               => true,
	        'show_ui'              => true,
			'menu_position'        => 20,
	        'capability_type'      => 'post',
	        'hierarchical'         => true,
	        'has_archive'          => false,
	        'exclude_from_search'  => false,
	        'show_in_nav_menus'    => true,
	        'supports'             => array('title', 'editor', 'thumbnail', 'excerpt', 'revisions'),
	        'rewrite'              => array('slug' => 'knowledgebase', 'with_front' => false),
	        'register_meta_box_cb' => 'mytheme_knowledgebase_metabox',
	       );

		/**
		 * Post type register
		 *
		 * @var     array     $args     Argument array for custom post type knowledgebase
		 * @since   1.0.0
		 */
	    register_post_type( 'knowledgebase' , $args );

	    /**
		 * Taxonomy register
		 *
		 * @since    1.0.0
		 */
	    register_taxonomy('knowledgebase_category', array('knowledgebase'), array('hierarchical' => true, 'label' => esc_html__('Categories', 'mytheme'), 'singular_label' => esc_html__('Category', 'mytheme'), 'rewrite' => true, 'slug' => 'knowledgebase_category','show_in_nav_menus'=>false));

	}
}

Notice the commenting. It’s important to comment your code. No matter how unimportant you think that could be, trust me, it’s better to comment it than not. So you can see that we’ve set up the knowledgebase CPT, and we’ve set up our labels for it, and all the interesting information about it like what menu icon it should have, what its capabilities are, if it has archive page etc. One of the important thing to notice is here, is the supports argument. This will enable things like TinyMCE editor, title functionality, featured image (thumbnail), excerpts etc. The rewrite part is important, as it will handle how your slug in the url will look like. Generally you can make this a translatable string, so if you have a multi language site, you can translate it, like

'rewrite' => array('slug' => __('knowledgebase', 'mytheme'), 'with_front' => false),

You can also make it appear in your REST API by specifying show_in_rest argument in the array. Also I’ve added register_meta_box_cb argument, which will provide a callback function that will be called when setting up the meta boxes for the edit form.
Last part in our function is the taxonomy. This is basically the custom category that goes with our custom post type.

Next, we’ll define our metabox funtion. In it you can have all sorts of things – input fields, select drop downs, textareas, checkboxes. You name it. If you have downloaded and tried our my plugin, you’ll notice that the talks section is a metabox, that has tabs in it, and you can even upload images in it. So the world is your oyster.

/**
 * Custom post type metaboxes
 *
 * @since		1.0.0
 */

if (!function_exists('mytheme_knowledgebase_metabox')) {
	function mytheme_knowledgebase_metabox() {
		add_meta_box('kb-metabox', esc_html__('Knowledgebase Details', 'abtheme'), 'mytheme_knowledgebase_info', 'knowledgebase', 'normal', 'high');
	}
}

/**
 * Custom post type metabox output
 *
 * @since		1.0.0
 */

if (!function_exists('mytheme_knowledgebase_info')) {
	function mytheme_knowledgebase_info(){
		global $post;

		if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ){
			return;
		}

		$custom             = get_post_custom($post->ID);
		$knowledgebase_info = (isset($custom["knowledgebase_info"][0])) ? $custom["knowledgebase_info"][0] : '';

		wp_nonce_field( 'knowledgebase_meta_box_nonce', 'knowledgebase_meta_nonce' );

		?>

		<style type="text/css">
			.kb_extras{margin-right: 10px;}
			.kb_extras label{display: block;}
			.kb_extras input{width: 50%;}
			.kb_extras input[type="checkbox"]{width: auto;}
			.kb_extras textarea{width: 100%;height: 300px;}
			.kb_extras .separator{padding-top: 20px;margin-top: 20px;border-top: 1px solid #ddd;}
		</style>

		<div class="kb_extras">
			<p>
				<label for="knowledgebase_info"><?php esc_html_e('Info:', 'abtheme')?></label>
				<input name="knowledgebase_info" id="knowledgebase_info" value="<?php echo esc_attr($knowledgebase_info); ?>" />
			</p>
		</div>
		<?php
	}
}

/**
 * Custom post type metabox save
 *
 * @since		1.0.0
 */

add_action('save_post', 'mytheme_kb_metabox_save');

if (!function_exists('mytheme_kb_metabox_save')) {
	function mytheme_kb_metabox_save(){
	    global $post;

	    if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ){
			return;
		}

		if( !isset( $_POST['knowledgebase_meta_nonce'] ) || !wp_verify_nonce( $_POST['knowledgebase_meta_nonce'], 'knowledgebase_meta_box_nonce' ) ) {
			return;
		}

		if( !current_user_can( 'edit_pages' ) ) {
			return;
		} elseif(is_object($post)){
	    	$knowledgebase_info = (isset($_POST['knowledgebase_info'])) ? $_POST['knowledgebase_info'] : '';
			update_post_meta($post->ID, 'knowledgebase_info', sanitize_text_field($knowledgebase_info));
	    }
	}
}

First we add our meta box (or more of them), then set up the function that will render it, and save it.

Knowledge base single post

Knowledge base single post with meta box added

Another thing that we can modify is the taxonomy screen. Say we want to custom order our taxonomy terms. We’d put an input field that will take in positive integers. I’ve added sidebar selectors here, colors, and even term images (like feature images) here. So this is also something that you can customize. Here we’ll take advantage of the termmeta table, that was introduced in the 4.4 version.

/**
 * Adding custom knowledgebase taxonomy field
 *
 * @var     object     $taxonomy     Taxonomy
 * @since   1.0.0
 */
add_action ( 'knowledgebase_category_add_form_fields', 'mytheme_extra_add_knowledgebase_category_fields', 10, 2);

if ( ! function_exists( 'mytheme_extra_add_knowledgebase_category_fields' ) ){
    function mytheme_extra_add_knowledgebase_category_fields( $taxonomy ) {
        ?>
        <div class="form-field">
            <label for="cat_order"><?php esc_attr_e('Category order', 'mytheme'); ?></label>
            <input type="text" name="cat_order" for="cat_order" id="cat_order" value="">
        </div>
        <?php
    }
}

/**
 * Adding custom knowledgebase taxonomy field in edit screen
 *
 * @var     object     $term     Term object for taxonomy
 * @since   1.0.0
 */
add_action ( 'knowledgebase_category_edit_form_fields', 'mytheme_extra_knowledgebase_category_fields');

if ( ! function_exists( 'mytheme_extra_knowledgebase_category_fields' ) ){
    function mytheme_extra_knowledgebase_category_fields( $term ) {
        $cat_order = get_term_meta( $term->term_id, 'cat_order', true );
        ?>
        <tr class="form-field">
            <th scope="row" valign="top"><label for="cat_order"><?php esc_attr_e('Category order', 'mytheme'); ?></label></th>
            <td><input type="text" name="cat_order" for="cat_order" id="cat_order" value="<?php echo intval($cat_order); ?>"></td>
        </tr>
    <?php
    }
}

/**
 * Saving extra custom taxonomy fields upon creation
 *
 * @var     array     $term_id     Knowledgebase taxonomy term id
 * @since   1.0.0
 */
add_action ( 'created_knowledgebase_category', 'mytheme_save_extra_knowledgebase_category_fileds');

if ( ! function_exists( 'mytheme_save_extra_knowledgebase_category_fileds' ) ){
    function mytheme_save_extra_knowledgebase_category_fileds( $term_id ) {
        if ( isset( $_POST['cat_order'] ) ) {
            $cat_order = intval($_POST['cat_order']);
            add_term_meta( $term_id, 'cat_order', $cat_order, true );
        }
    }
}

/**
 * Saving extra custom taxonomy fields upon edit
 *
 * @var     array     $term_id     Knowledgebase taxonomy term id
 * @since   1.0.0
 */
add_action ( 'edited_knowledgebase_category', 'mytheme_update_extra_knowledgebase_category_fileds');

if ( ! function_exists( 'mytheme_update_extra_knowledgebase_category_fileds' ) ){
    function mytheme_update_extra_knowledgebase_category_fileds( $term_id ) {
        if ( isset( $_POST['cat_order'] ) ) {
            $cat_order = intval($_POST['cat_order']);
            update_term_meta( $term_id, 'cat_order', $cat_order, false );
        }
    }
}

This will give us an input field where we can add number for our category, both in the edit category term screen and in the create screen. If we want to display that information (and make it sortable) in the table where the name of the categories are, we’ll need to add another piece of code.

/**
 * Adding custom column to knowledgebase taxonomy
 *
 * @var     array     $cat_columns     Columns array in custom taxonomy
 * @return  array     Returns updated category columns
 * @since   1.0.0
 */
add_filter('manage_edit-knowledgebase_category_columns', 'mytheme_cat_order_column', 10, 2);

if (!function_exists('mytheme_cat_order_column')) {
    function mytheme_cat_order_column( $cat_columns ){
        $cat_columns['cat_order'] = esc_attr__('Category Order', 'mytheme');
        return $cat_columns;
    }
}

/**
 * Managing custom column in knowledgebase taxonomy display
 *
 * @var     array     $deprecated
 * @var     array     $column_name     Column name
 * @var     array     $term_id     Knowledgebase taxonomy term id
 * @since   1.0.0
 */
add_filter ('manage_knowledgebase_category_custom_column', 'mytheme_manage_knowledgebase_category_custom_fields', 10,3);

if (!function_exists('mytheme_manage_knowledgebase_category_custom_fields')) {
    function mytheme_manage_knowledgebase_category_custom_fields($deprecated, $column_name, $term_id){
        if ($column_name == 'cat_order') {
        	$cat_order = get_term_meta( $term_id, 'cat_order', true );
            if (isset($cat_order) && $cat_order != '') {
               echo intval($cat_order);
            }
        }
    }
}

/**
 * Make column sortable
 *
 * @param  array $sortable Sortable columns array
 * @return array           Updated sortable columns array
 * @since   1.0.0
 */
add_filter('manage_edit-knowledgebase_category_sortable_columns', 'mytheme_manage_knowledgebase_category_sortable_columns');

if(!function_exists('mytheme_manage_knowledgebase_category_sortable_columns')){
    function mytheme_manage_knowledgebase_category_sortable_columns($sortable){
        $sortable['cat_order'] = 'cat_order';
        return $sortable;
    }
}

/**
 * Custom column sortable query
 *
 * @param  array $pieces     Terms query SQL clauses.
 * @param  array $taxonomies An array of taxonomies.
 * @param  array $args       An array of terms query arguments.
 * @return array             Modified query array
 * @since   1.0.0
 */

add_filter( 'terms_clauses', 'mytheme_filter_custom_terms', 10, 3 );

if (!function_exists('mytheme_filter_custom_terms')) {
	function mytheme_filter_custom_terms( $pieces, $taxonomies, $args ) {

		global $wpdb;

		$orderby = isset($_REQUEST['orderby']) ? trim(wp_unslash($_REQUEST['orderby'])) : 'cat_order';
		if($orderby == 'cat_order' ) {
			$pieces['fields'] .= ", tm.*";
			$pieces['join']   .= " INNER JOIN {$wpdb->termmeta} AS tm ON tt.term_id = tm.term_id";
	    	$pieces['orderby'] = "ORDER BY ABS(tm.meta_value)";
		}

	    return $pieces ;

	}
}

First function will add the new column, the second will populate it with our term meta, third will make that column sortable, but we need to modify our sorting query. This is where last part comes in handy. The idea is the same with the sortable columns for posts, but for terms we need to use terms_clauses hook, to execute our query. The query itself is an interesting thing (at least to me). Full query, as you’re seeing in the code, looks like this:

SELECT t.*, tt.*, tm.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id INNER JOIN wp_termmeta AS tm ON tt.term_id = tm.term_id WHERE tt.taxonomy IN ('knowledgebase_category') ORDER BY ABS(tm.meta_value) ASC

But here we’re using $pieces array that, before we modify it looks like this:

Array (
	[fields] => t.*, tt.*
	[join] => INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id
	[where] => tt.taxonomy IN ('knowledgebase_category')
	[distinct] =>
	[orderby] => ORDER BY t.name
	[order] => ASC
	[limits] =>
)

And after modification looks like this

Array (
	[fields] => t.*, tt.*, tm.*
	[join] => INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id INNER JOIN wp_termmeta AS tm ON tt.term_id = tm.term_id
	[where] => tt.taxonomy IN ('knowledgebase_category')
	[distinct] =>
	[orderby] => ORDER BY ABS(tm.meta_value)
	[order] => ASC
	[limits] =>
)

You can see that you’ll need to put your query in a certain keys for it to work. It’s just WordPress way of handling the custom sorting. If you were to custom sort columns in custom post screen, you’d use pre_get_posts hook. The same principle applies there. You’ll end up with something like this:

Knowledge base category screen

Knowledge base category screen

If you are wondering why I had to use custom sort, it’s because no matter what I tried, I couldn’t get them to sort. So I needed some small MySQL magic for it to work. Notice that the order parameter is absolute value of the term meta field. This limits the values to positive integers if you want the sort to work. Because the absolute value will turn negative values to positive, and the sorting wouldn’t work. There is probably a better way to handle this, but it’s good for demonstrations sake. Using term meta is a great thing since you already have this table in your database (you might as well use it, no?), plus when you remove your term, the term meta value will get removed immediately as well, which is great way to insure your database isn’t filled with junk values that serve no purpose :)

But wait! There’s more…

This is a task I admit, but I want to show you what you can do with CPT. So one more thing that you can add to it is custom column in the CPT screen. Say that you want to show categories that are associated with each post. Then just define these:

/**
 * Adding custom column to knowledgebase post type
 *
 * @var     array     $columns     Columns array in custom post type
 * @return  array     Returns updated category columns
 * @since   1.0.0
 */
add_filter('manage_knowledgebase_posts_columns', 'mytheme_knowledgebase_column', 10, 2);

if (!function_exists('mytheme_knowledgebase_column')) {
    function mytheme_knowledgebase_column( $columns ){
    	$columns = array(
			'cb'                => '<input type="checkbox" />',
			'title'             => esc_html__('Knowledgebase title', 'mytheme'),
			'knowledgebase_cat' => esc_html__('Knowledgebase category', 'mytheme'),
			'date'              => __( 'Date' )
		);
        return $columns;
        /*Alternative:
			$columns['knowledgebase_cat'] = esc_html__('Knowledgebase category', 'mytheme');
			but this puts it at the end...
			http://code.tutsplus.com/articles/add-a-custom-column-in-posts-and-custom-post-types-admin-screen--wp-24934
        */
    }
}

/**
 * Managing custom column in knowledgebase post type display
 *
 * @var     array     $column_name     Column name
 * @var     array     $post_id         Knowledgebase post post id
 * @since   1.0.0
 */
add_filter ('manage_knowledgebase_posts_custom_column', 'mytheme_manage_knowledgebase_custom_fields', 10,3);

if (!function_exists('mytheme_manage_knowledgebase_custom_fields')) {
    function mytheme_manage_knowledgebase_custom_fields( $column_name, $post_id){
        if ($column_name == 'knowledgebase_cat') {
        	$terms = get_the_terms($post_id, 'knowledgebase_category');
        	if (!empty($terms)) {
        		foreach ($terms as $term => $term_value) {
        			$term_arr[] = $term_value->name;
        		}
        		echo implode(', ', $term_arr);
        	}
        }
    }
}

/**
 * Make column sortable
 *
 * @param  array $sortable Sortable columns array
 * @return array           Updated sortable columns array
 * @since   1.0.0
 */
add_filter('manage_edit-knowledgebase_sortable_columns', 'mytheme_manage_knowledgebase_sortable_columns');

if(!function_exists('mytheme_manage_knowledgebase_sortable_columns')){
    function mytheme_manage_knowledgebase_sortable_columns($sortable){
        $sortable['knowledgebase_cat'] = 'knowledgebase_cat';
        return $sortable;
    }
}

Notice that we’ve added the custom column by specifying the default fields, and then inserted the column where we want it. The advantage of this is that we can control where we put the column. The

$columns['knowledgebase_cat'] = esc_html__('Knowledgebase category', 'mytheme');

approach will always put your column at the end. And that could look odd. Imploding the terms is better then just echoing them out, because you can delimit them with any delimiter you want (in my case it’s a comma), and you won’t have a delimiter after the last array value. Since this column is showing letters, the default alphabetic sorting will work here.

Knowledge base posts screen

Knowledge base posts screen

You can put what ever column you want – custom post meta values, last modified date. Anything you might think is important to show. The general rule is not to put unnecessary stuff in the columns. If it provides no useful information, leave it out ;)

And this is it! You’ve created your own custom post type. Congratulations. You see that you can do a lot with it. The greatest thing is that you can separate the posts further, and use it in custom queries later on. Better than bunching it all together in regular posts :D I hope this helped you and if you have any kind of question feel free to post it in the comments below. Happy coding!


Also published on Medium.

Join the Discussion