Apply template to custom post type

Typically I wouldn’t follow such a strong answer like the one @Milo provided with a post like this. Since I already had this code written for another project I thought I’d share.

The code below does everything @Milo summarized in his answer and I’ve ported this across projects before with great success.

Here’s the cheat sheet for what’s going on:

1) Hook into the ‘add_meta_boxes’ action to render a new custom meta box on the edit screen (except on the native ‘page’ post type).

2) Hook into the ‘admin_menu’ action to remove the existing ‘Page Attributes’ meta box (except on the native ‘page’ post type).

3) Build out the custom meta box to replace the functionality of the native ‘Page Attributes’ meta box. This includes fields for defining the PARENT, TEMPLATE and ORDER of any custom post type you’ve initialized.

4) Hook into the ‘save_post’ action to save your template selection in the post meta.

5) Hook into the ‘single_template’ filter to load your custom template instead of the default WordPress template.

Here’s is the fully functional copy/paste for you:

/** Custom Post Type Template Selector **/
function cpt_add_meta_boxes() {
    $post_types = get_post_types();
    foreach( $post_types as $ptype ) {
        if ( $ptype !== 'page') {
            add_meta_box( 'cpt-selector', 'Attributes', 'cpt_meta_box', $ptype, 'side', 'core' );
        }
    }
}
add_action( 'add_meta_boxes', 'cpt_add_meta_boxes' );

function cpt_remove_meta_boxes() {
    $post_types = get_post_types();
    foreach( $post_types as $ptype ) {
        if ( $ptype !== 'page') {
            remove_meta_box( 'pageparentdiv', $ptype, 'normal' );
        }
    }
}
add_action( 'admin_menu' , 'cpt_remove_meta_boxes' );

function cpt_meta_box( $post ) {
    $post_meta = get_post_meta( $post->ID );
    $templates = wp_get_theme()->get_page_templates();

    $post_type_object = get_post_type_object($post->post_type);
    if ( $post_type_object->hierarchical ) {
        $dropdown_args = array(
            'post_type'        => $post->post_type,
            'exclude_tree'     => $post->ID,
            'selected'         => $post->post_parent,
            'name'             => 'parent_id',
            'show_option_none' => __('(no parent)'),
            'sort_column'      => 'menu_order, post_title',
            'echo'             => 0,
        );

        $dropdown_args = apply_filters( 'page_attributes_dropdown_pages_args', $dropdown_args, $post );
        $pages = wp_dropdown_pages( $dropdown_args );

        if ( $pages ) { 
            echo "<p><strong>Parent</strong></p>";
            echo "<label class=\"screen-reader-text\" for=\"parent_id\">Parent</label>";
            echo $pages;
        }
    }

    // Template Selector
    echo "<p><strong>Template</strong></p>";
    echo "<select id=\"cpt-selector\" name=\"_wp_page_template\"><option value=\"default\">Default Template</option>";
    foreach ( $templates as $template_filename => $template_name ) {
        if ( $post->post_type == strstr( $template_filename, '-', true) ) {
            if ( isset($post_meta['_wp_page_template'][0]) && ($post_meta['_wp_page_template'][0] == $template_filename) ) {
                echo "<option value=\"$template_filename\" selected=\"selected\">$template_name</option>";
            } else {
                echo "<option value=\"$template_filename\">$template_name</option>";
            }
        }
    }
    echo "</select>";

    // Page order
    echo "<p><strong>Order</strong></p>";
    echo "<p><label class=\"screen-reader-text\" for=\"menu_order\">Order</label><input name=\"menu_order\" type=\"text\" size=\"4\" id=\"menu_order\" value=\"". esc_attr($post->menu_order) . "\" /></p>";
}

function save_cpt_template_meta_data( $post_id ) {

    if ( isset( $_REQUEST['_wp_page_template'] ) ) {
        update_post_meta( $post_id, '_wp_page_template', $_REQUEST['_wp_page_template'] );
    }
}
add_action( 'save_post' , 'save_cpt_template_meta_data' );

function custom_single_template($template) {
    global $post;

    $post_meta = ( $post ) ? get_post_meta( $post->ID ) : null;
    if ( isset($post_meta['_wp_page_template'][0]) && ( $post_meta['_wp_page_template'][0] != 'default' ) ) {
        $template = get_template_directory() . "https://wordpress.stackexchange.com/" . $post_meta['_wp_page_template'][0];
    }

    return $template;
}
add_filter( 'single_template', 'custom_single_template' );
/** END Custom Post Type Template Selector **/

The only assumption I’ve made here is that your templates follow a naming convention best practice of:

posttype-templatename.php

As an example you could define some custom templates for an “Event” custom post type using the following naming convention within your theme:

event-standard.php
event-allday.php
event-recurring.php

This code is smart enough to only allow “event” templates to be applied to the Event post type. In other words, a template called “section-video.php” template would never be visible to the Event post type. That template would instead appear as an option on the “Section” post type.

To remove this feature you simply need to remove the conditional logic from the code above:

if ( $post->post_type == strstr( $template_filename, '-', true) ) {  }

Leave a Comment