Create WordPress theme from scratch : Part 11 – Custom Post Types

Welcome to Part 11 of the series Create WordPress theme from scratch. In this tutorial, we will look at one of the best features available in WordPress – Custom Post Types.

If you missed the Part 10 of this series, then check it out

Getting started with Custom Post Types

Introduced in WordPress version 3.0, Custom Post Types are one of the best features added to WordPress ever. This is the feature that took WordPress to the next level. This game changer feature of WordPress enabled support for managing almost anything that a user will ever need.

Custom Post Types is a plugin territory functionality that should not be included in a theme. Making plugins for custom post types makes certain that the post type remains usable when the user switches to another theme.

The only reason to include this feature in a theme is that we want to offer this feature as a part of the theme itself. That’s exactly what I plan to do.

Setting up Custom Post Type

We are going to add a new post type called Book to our theme. To keep the functions file under control, we are going to place our custom post type code in a separate file. Create a new file called custom-post-book.php in the inc directory.

Add the following lines of code at the end of the functions.php file to include our new file.

require_once TEMPLATE_PATH . '/inc/custom-post-book.php';

Add the following lines of PHP code to the newly created file which adds our custom post type.

add_action( 'init', 'mytheme_book_posts_register' );

function mytheme_book_posts_register() {
    $labels = array (
        'name'                   => __( 'Books', 'mytheme' ),
        'all_items'              => __( 'All Books', 'mytheme' ),
        'name_admin_bar'         => __( 'Book', 'mytheme' ),
        'add_new'                => __( 'Add New Book', 'mytheme' ),
        'add_new_item'           => __( 'Add New Book', 'mytheme' ),
    );
    $args = array(
        'labels'                 => $labels,
        'public'                 => true,
        'show_ui'                => true,
        'menu_icon'              => 'dashicons-book',
        'taxonomies'             => array( 'mytheme_book_genre', 'mytheme_book_writer' ),
        'hierarchical'           => false,
        'supports'               => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'has_archive'            => true,
        'rewrite'                => array( 'slug' => 'books' ),
        'register_meta_box_cb'   => 'mytheme_register_book_meta_boxes',
    );
    register_post_type( 'mytheme_book', $args );
}

We start by adding a callback function to the init hook. All custom post types must be added using this hook.

add_action( 'init', 'mytheme_book_posts_register' );

Inside our callback function, we set up the arguments needed by our custom post type. This function uses the register_post_type function which performs the actual post registration. We are setting up the following arguments for the register_post_type function.

  • labels – An array of labels to be used in the admin area.
  • public – When enabled, makes post available to authors and readers.
  • show_ui – When enabled, generate an interface to manage this post type.
  • menu_icon – A menu icon to make it easily identifiable.
  • taxonomies – To connect custom post type to custom taxonomy items.
  • hierarchical – Enable or disable page like hierarchy.
  • supports – Add support for features (here we add title, thumbnail, post editor and excerpt).
  • has_archive – Enable or disable archives page for this post type.
  • rewrite – The permalink where an index of this post type will be available.
  • register_meta_box_cb – A callback function to register meta boxes for this post.

Specifying the taxonomies arguments makes certain that we don’t end up with unexpected results and failures due to an invalid connection between post types and taxonomy terms. We will create our taxonomies in a while.

The rewrite rules need to be flushed before the custom post type archive or single  pages can be accessed. Go to Dashboard > Settings > Permalinks and re-save the existing settings. This will flush the rewrite rules. Without flushing the rewrite rules, WordPress will end up with a 404 error.

The register_post_type function can be configured with a wide range of argument. Check the official codex documentation for a complete list of available options.

Next, we add our custom taxonomy. Edit the file custom-post-book.php and add the following lines of code at the end.

add_action( 'init', 'mytheme_book_taxonomy_register' );

function mytheme_book_taxonomy_register()
{
    $labels = array(
        'name'                => _x( 'Genres', 'Taxonomy general name', 'mytheme' ),
        'all_items'           => __( 'All Genres', 'mytheme' ),
    );
    $args = array(
        'labels'              => $labels,
        'public'              => true,
        'show_ui'             => true,
        'show_admin_column'   => true,
        'hierarchical'        => true,
        'query_var'           => true,
        'rewrite'             => array( 'slug' => 'book-generes' ) ,
    );
    register_taxonomy( 'mytheme_book_genre', array( 'mytheme_book' ), $args );

    $labels = array(
        'name'                => _x( 'Writers', 'Taxonomy general name', 'mytheme' ),
        'all_items'           => __( 'All Writers', 'mytheme' ),
    );
    $args     = array(
        'labels'              => $labels,
        'public'              => true,
        'show_ui'             => true,
        'show_admin_column'   => true,
        'hierarchical'        => false,
        'query_var'           => true,
        'rewrite'             => array( 'slug' => 'book-writers' ),
    );
    register_taxonomy( 'mytheme_book_writer', array( 'mytheme_book' ), $args );
}

Once again, we are using the init hook here. Just like custom post types, all custom taxonomy items must be registered using the init hook. The callback function attached to the init hook registers our custom taxonomy terms.

add_action( 'init', 'mytheme_book_taxonomy_register' );

The callback function sets up arguments needed by our taxonomy terms. To add the taxonomy term we use the register_taxonomy function. We supply the register_taxonomy function with the following arguments

  • labels – An array of labels to be used in the admin area.
  • public – When enabled, makes taxonomy publicly queryable.
  • show_ui – When enabled, generate an interface to manage the taxonomy.
  • show_admin_column – When enabled, automatically adds taxonomy column on associated post-types table.
  • hierarchical – If enabled, taxonomy behaves like categories. When disabled behaves like tags.
  • query_var – Enable query vars to enable direct query using WP_Query.
  • rewrite – Permalink where taxonomy item will be available. e.g. book-writers/mr-x/

Notice that we are registering two taxonomy terms. One is the genre which behaves like a category and the other one is the writer which behaves like a tag. This, of course, is just an example. You can create anything you want.

Just like the function register_post_type, the function register_taxonomy too can be configured using an extensive list of arguments. Check the codex link for comprehensive info.

Remember the callback we added to register_post_type earlier. We are going to use this callback function now to render our meta boxes.

'register_meta_box_cb'   => 'mytheme_register_book_meta_boxes'

Paste the following lines of code at the end of the functions file.

function mytheme_register_book_meta_boxes( $post ) {
    add_meta_box(
        'mytheme_books_meta',
        __( 'Book Details', 'mytheme' ),
        'mytheme_render_book_meta',
        array( 'mytheme_book' ),
        'advanced',
        'high'
    );
}

Using our callback, we are adding the meta box to our custom post type. The function add_meta_box adds a meta box to a screen. It  accepts the following arguments

  • id – Meta box ID.
  • title – Meta box title.
  • callback – Function that renders the meta box content.
  • screen – An id or any array of screens where to show the meta box.
  • context – Sets where to display the meta box. This differs from screen to screen.
  • priority – The priority within the context where the boxes should show.
  • callback_args – Additional data passed to the second argument of the callback function.

Next, paste the following lines of code at the end of our custom-post-book.php.

function mytheme_book_meta_fields() {
    return array(
        'date_published'     => __( 'Date Published', 'mytheme' ),
        'publisher'          => __( 'Publisher', 'mytheme' ),
        'edition'            => _x( 'Edition', 'paperback, pdf, epub...', 'mytheme' ),
        'pages'              => __( 'Pages', 'mytheme' ),
    );
}

function mytheme_render_book_meta( $post )
{
    $options = mytheme_book_meta_fields();
    if ( ! sizeof( $options ) ) {
        return;
    }

    wp_nonce_field( 'mytheme_save_book_meta', 'mytheme_book_meta_nonce' );
    $book_meta = get_post_custom( $post->ID );

    echo '<table>';
    foreach ( $options as $id => $label ) {
        $value = isset( $book_meta[$id][0] ) ? $book_meta[$id][0] : '';
        ?>
        <tr>
            <td><label for="<?php echo esc_attr( $id ); ?>" style="margin-right: 25px;"><?php echo $label; ?></label></td>
            <td><input type="text" name="<?php echo esc_attr( $id ); ?>" value="<?php echo esc_attr( $value ); ?>" size="30"></td>
        </tr>
        <?php
    }
    echo '</table>';
}

The function mytheme_book_meta_fields returns an associative array of id and labels of the options available. Though there are better ways to do this but for this tutorial, this is enough.

function mytheme_book_meta_fields() {
    return array(
        'date_published'     => __( 'Date Published', 'mytheme' ),
        'publisher'          => __( 'Publisher', 'mytheme' ),
        'edition'            => _x( 'Edition', 'paperback, pdf, epub...', 'mytheme' ),
        'pages'              => __( 'Pages', 'mytheme' ),
    );
}

As you can see, we are going to create four meta fields – publish date, publisher, edition and number of pages.

Inside the callback function, we start with grabbing the meta fields.

$options = mytheme_book_meta_fields();
if ( ! sizeof( $options ) ) {
    return;
}

After that, we add something called a nonce field.

wp_nonce_field( 'mytheme_save_book_meta', 'mytheme_book_meta_nonce' );

A nonce is a one-time security token generated by a site to identify future requests to that site. When a request is submitted, the nonce field is verified before processing the request. The request will be rejected if the nonce can’t be verified.

This security measure prevents unexpected or malicious code from running which could cause permanent damage to a site, especially to the database. The nonce value is normally included as a hidden form field in the HTML.

We are adding the nonce field using function wp_nonce_field which can take four optional arguments. Always use the first two fields to provide better security. Check the codex link for more info.

After the nonce field, we retrieve all the meta information of the current post.

$book_meta = get_post_custom( $post->ID );

The function get_post_custom returns all the custom fields of a post in a multi-dimensional array.

Next, we loop through our fields and display them in a table. For each field, we grab the first value of the multi-dimensional array.

echo '<table>';
foreach ( $options as $id => $label ) {
    $value = isset( $book_meta[$id][0] ) ? $book_meta[$id][0] : '';
    ?>
    <tr>
        <td><label for="<?php echo esc_attr( $id ); ?>" style="margin-right: 25px;"><?php echo $label; ?></label></td>
        <td><input type="text" name="<?php echo esc_attr( $id ); ?>" value="<?php echo esc_attr( $value ); ?>" size="30"></td>
    </tr>
    <?php
}
echo '</table>';
Create WordPress theme from scratch Book Meta Fields

Now that we have our custom meta fields in place, we need to save them when the user saves the post. Add the following lines of code at the end of our custom post file.

add_action( 'save_post_mytheme_book', 'mytheme_save_book_meta' );

function mytheme_save_book_meta( $post_id )
{
    if ( ! isset( $_POST['mytheme_book_meta_nonce'] ) ) {
        return;
    }

    if ( ! wp_verify_nonce( $_POST['mytheme_book_meta_nonce'], 'mytheme_save_book_meta' ) ) {
        return;
    }

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

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    $options = mytheme_book_meta_fields();

    foreach ( $options as $id => $label ) {
        if ( isset($_POST[$id] ) ) {
            $value = sanitize_text_field( $_POST[$id] );
            update_post_meta( $post_id, $id, $value );
        }
    }
}

To save our custom fields, we are attaching a function to a custom hook save_post_mytheme_book that WordPress generates. This hook will only trigger when our custom post type is saved. Comprehensive information on the save action hook is available here.

Inside our callback function, we start with checking the presence of our nonce field.

if ( ! isset($_POST['mytheme_book_meta_nonce'] ) ) {
    return;
}

If the nonce field is present, then we verify it using wp_verify_nonce.

if ( ! wp_verify_nonce( $_POST['mytheme_book_meta_nonce'], 'mytheme_save_book_meta' ) ) {
    return;
}

If WordPress is doing an autosave, then we bail.

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

Next, we check if the current user has the permission to edit the current post. Remember, our custom post uses the post privileges.

if ( ! current_user_can( 'edit_post', $post_id ) ) {
    return;
}

The function current_user_can returns a Boolean value – whether the current user has capability or role.

Finally, if all the validation succeeds, we save the custom fields to the database.

$options = mytheme_book_meta_fields();

foreach ( $options as $id => $label ) {
    if ( isset($_POST[$id] ) ) {
        $value = sanitize_text_field( $_POST[$id] );
        update_post_meta( $post_id, $id, $value );
    }
}

We sanitize the data using sanitize_text_field. Maybe, I have over-simplified the above code. There are certainly far better ways to do this.

Back in the Dashboard, our Books listing looks like this

Create WordPress theme from scrtch All Books

 

Let’s change it to include our custom meta fields. Add the following lines of code at the end of our custom post file.

add_filter( 'manage_edit-mytheme_book_columns', 'mytheme_book_columns' );
function mytheme_book_columns( $columns ) {
    return array(
        'cb'                            => '<input type="checkbox" />',
        'title'                         => 'Title',
        'taxonomy-mytheme_book_genre'   => 'Genres',
        'taxonomy-mytheme_book_writer'  => 'Writers',
        'excerpt'                       => 'Description',
        'published'                     => 'Date published',
        'pages'                         => 'pages',
        'edition'                       => 'Edition',
        'publisher'                     => 'Publisher',
        'date'                          => 'Date',
    );
}

add_action( 'manage_mytheme_book_posts_custom_column', 'mytheme_book_custom_column', 10, 2 );
function mytheme_book_custom_column( $column_name, $post_id ) {
    $post_meta = get_post_custom( $post_id );

    switch ( $column_name ) {
        case 'excerpt':
            the_excerpt();
            break;

        case 'published':
            echo $post_meta['date_published'][0];
            break;

        case 'pages';
            echo $post_meta['pages'][0];
            break;

        case 'edition':
            echo $post_meta['edition'][0];
            break;

        case 'publisher':
            echo $post_meta['publisher'][0];
            break;
    }
}

First, we need to change the columns listed by WordPress. This is done using a filter available for custom post types – manage_$post_type_posts_columns.

add_filter( 'manage_edit-mytheme_book_columns', 'mytheme_book_columns' );

In the callback function attached to the hook, we return an associative array of fields we want to be listed.

function mytheme_book_columns( $columns ) {
    return array(
        'cb'                            => '<input type="checkbox" />',
        'title'                         => 'Title',
        'taxonomy-mytheme_book_genre'   => 'Genres',
        'taxonomy-mytheme_book_writer'  => 'Writers',
        'excerpt'                       => 'Description',
        'published'                     => 'Date published',
        'pages'                         => 'pages',
        'edition'                       => 'Edition',
        'publisher'                     => 'Publisher',
        'date'                          => 'Date',
    );
}

Using the filter alters the columns that WordPress will list but it does nothing to display the data. To render the data we need to make use of a hook manage_$post_type_posts_custom_column available to custom posts.

add_action( 'manage_mytheme_book_posts_custom_column', 'mytheme_book_custom_column', 10, 2 );

The callback function attached to this hook receives the column name to display and the id of the post being displayed. With this information, we can display any data we want.

function mytheme_book_custom_column( $column_name, $post_id ) {
    $post_meta = get_post_custom( $post_id );

    switch ( $column_name ) {
        case 'excerpt':
            the_excerpt();
            break;

        case 'published':
            echo $post_meta['date_published'][0];
            break;

        case 'pages';
            echo $post_meta['pages'][0];
            break;

        case 'edition':
            echo $post_meta['edition'][0];
            break;

        case 'publisher':
            echo $post_meta['publisher'][0];
            break;
    }
}

The final listing looks like this:
Custom Column Listing
Here is the completed custom post file custom-post-book.php.

add_action( 'init', 'mytheme_book_posts_register' );
function mytheme_book_posts_register() {
    $labels = array (
        'name'                   => __( 'Books', 'mytheme' ),
        'all_items'              => __( 'All Books', 'mytheme' ),
        'name_admin_bar'         => __( 'Book', 'mytheme' ),
        'add_new'                => __( 'Add New Book', 'mytheme' ),
        'add_new_item'           => __( 'Add New Book', 'mytheme' ),
    );
    $args = array(
        'labels'                 => $labels,
        'public'                 => true,
        'show_ui'                => true,
        'menu_icon'              => 'dashicons-book',
        'taxonomies'             => array( 'mytheme_book_genre', 'mytheme_book_writer' ),
        'hierarchical'           => false,
        'supports'               => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'has_archive'            => true,
        'rewrite'                => array( 'slug' => 'books' ),
        'register_meta_box_cb'   => 'mytheme_register_book_meta_boxes',
    );
    register_post_type( 'mytheme_book', $args );
}

add_action( 'init', 'mytheme_book_taxonomy_register' );
function mytheme_book_taxonomy_register()
{
    $labels = array(
        'name'               => _x( 'Genres', 'Taxonomy general name', 'mytheme' ),
        'all_items'          => __( 'All Genres', 'mytheme' ),
    );
    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'show_ui'            => true,
        'show_admin_column'  => true,
        'hierarchical'       => true,
        'query_var'          => true,
        'rewrite'            => array( 'slug' => 'book-generes' ) ,
    );
    register_taxonomy( 'mytheme_book_genre', array( 'mytheme_book' ), $args );

    $labels = array(
        'name'               => _x( 'Writers', 'Taxonomy general name', 'mytheme' ),
        'all_items'          => __( 'All Writers', 'mytheme' ),
    );
    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'show_ui'            => true,
        'show_admin_column'  => true,
        'hierarchical'       => false,
        'query_var'          => true,
        'rewrite'            => array( 'slug' => 'book-writers' ),
    );
    register_taxonomy( 'mytheme_book_writer', array( 'mytheme_book' ), $args );
}

function mytheme_register_book_meta_boxes( $post ) {
    add_meta_box(
        'mytheme_books_meta',
        __( 'Book Details', 'mytheme' ),
        'mytheme_render_book_meta',
        array( 'mytheme_book' ),
        'advanced',
        'high'
    );
}

function mytheme_book_meta_fields() {
    return array(
        'date_published'    => __( 'Date Published', 'mytheme' ),
        'publisher'         => __( 'Publisher', 'mytheme' ),
        'edition'           => _x( 'Edition', 'paperback, pdf, epub...', 'mytheme' ),
        'pages'             => __( 'Pages', 'mytheme' ),
    );
}

function mytheme_render_book_meta( $post )
{
    $options = mytheme_book_meta_fields();
    if ( ! sizeof( $options ) ) {
        return;
    }

    wp_nonce_field( 'mytheme_save_book_meta', 'mytheme_book_meta_nonce' );
    $book_meta = get_post_custom( $post->ID );

    echo '<table>';
    foreach ( $options as $id => $label ) {
        $value = isset( $book_meta[$id][0] ) ? $book_meta[$id][0] : '';
        ?>
        <tr>
            <td><label for="<?php echo esc_attr($id ); ?>" style="margin-right: 25px;"><?php echo $label; ?></label></td>
            <td><input type="text" name="<?php echo esc_attr( $id ); ?>" value="<?php echo esc_attr( $value ); ?>" size="30"></td>
        </tr>
        <?php
    }
    echo '</table>';
}

add_action( 'save_post_mytheme_book', 'mytheme_save_book_meta' );
function mytheme_save_book_meta( $post_id )
{
    if ( ! isset( $_POST['mytheme_book_meta_nonce'] ) ) {
        return;
    }

    if ( ! wp_verify_nonce( $_POST['mytheme_book_meta_nonce'], 'mytheme_save_book_meta' ) ) {
        return;
    }

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

    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    $options = mytheme_book_meta_fields();

    foreach ( $options as $id => $label ) {
        if ( isset($_POST[$id] ) ) {
            $value = sanitize_text_field( $_POST[$id] );
            update_post_meta( $post_id, $id, $value );
        }
    }
}

add_filter( 'manage_edit-mytheme_book_columns', 'mytheme_book_columns' );
function mytheme_book_columns( $columns ) {
    return array(
        'cb'                            => '<input type="checkbox" />',
        'title'                         => 'Title',
        'taxonomy-mytheme_book_genre'   => 'Genres',
        'taxonomy-mytheme_book_writer'  => 'Writers',
        'excerpt'                       => 'Description',
        'published'                     => 'Date published',
        'pages'                         => 'pages',
        'edition'                       => 'Edition',
        'publisher'                     => 'Publisher',
        'date'                          => 'Date',
    );
}

add_action( 'manage_mytheme_book_posts_custom_column', 'mytheme_book_custom_column', 10, 2 );
function mytheme_book_custom_column( $column_name, $post_id ) {
    $post_meta = get_post_custom( $post_id );

    switch ( $column_name ) {
        case 'excerpt':
            the_excerpt();
            break;

        case 'published':
            echo $post_meta['date_published'][0];
            break;

        case 'pages';
            echo $post_meta['pages'][0];
            break;

        case 'edition':
            echo $post_meta['edition'][0];
            break;

        case 'publisher':
            echo $post_meta['publisher'][0];
            break;
    }
}

Conclusion

This concludes the eleventh part of the series Create WordPress theme from scratch. In the next and final part of the series, we will integrate our custom post into our theme.

Virtual Reality Headsets

Next Article

13 Virtual Reality Headsets You Should Own To Experience Virtual Reality