Adding Page Attributes Metabox and Page Templates to the Posts Edit Page?

(Moderators note: Title was originally “How can I add the “Page Attributes” and/or “Page Attributes > Template” selector to POSTS editor”)

WP currently only allows the assignment of a “template” to Pages (i.e. post_type=='page'.) I’d like to extend this functionality to Posts as well (i.e. post_type=='post'.)

How can I add the “Page Attributes” meta box and more specifically, the template switcher to the posts editor?

I’m assuming this is code I will place in my functions.php for my theme.

UPDATE: I’ve managed to add the hardcoded templates pulldown menu to my post editor, by simply adding the select box html to my existing custom meta options box. Here’s the code I’m using for that…

add_meta_box('categorydiv2', __('Post Options'), 'post_categories_meta_box_modified', 'post', 'side', 'high');

And here’s the function that writes out the options and the template select box…

//adds the custom categories box
function post_categories_meta_box_modified() {
    global $post;
    if( get_post_meta($post->ID, '_noindex', true) ) $noindexChecked = " checked='checked'";
    if( get_post_meta($post->ID, '_nofollow', true) ) $nofollowChecked = " checked='checked'";
?>
<div id="categories-all" class="ui-tabs-panel">
    <ul id="categorychecklist" class="list:category categorychecklist form-no-clear">
        <li id='noIndex' class="popular-category"><label class="selectit"><input value="noIndex" type="checkbox" name="chk_noIndex" id="chk_noIndex"<?php echo $noindexChecked ?> /> noindex</label></li> 
        <li id='noFollow' class="popular-category"><label class="selectit"><input value="noFollow" type="checkbox" name="chk_noFollow" id="chk_noFollow"<?php echo $nofollowChecked ?> /> nofollow</label></li>
    </ul>

    <p><strong>Template</strong></p> 
    <label class="screen-reader-text" for="page_template">Post Template</label><select name="page_template" id="page_template"> 
    <option value="default">Default Template</option> 
    <option value="template-wide.php" >No Sidebar</option>
    <option value="template-salespage.php" >Salespage</option>
    </select>
</div>
<?php
}

And finally, the code to capture the selected values on save…

function save_post_categories_meta($post_id) {
    if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) return $post_id;
    $noIndex = $_POST['chk_noIndex'];
    $noFollow = $_POST['chk_noFollow'];
    update_post_meta( $post_id, '_noindex', $noIndex );
    update_post_meta( $post_id, '_nofollow', $noFollow );
    return $post_id;
}

Now, I believe all that’s left is (1) capturing the selected template and adding it to the post meta for this post and (2) modifying index.php and single.php so that it uses the chosen template.

2

Hate to be the bearer of bad news but WordPress hardcodes the Page Template functionality to the “page” post type, at least in v3.0 (that might change in future versions but there’s not a specific initiative I’m aware of to change it yet. So this is one of the very few times I’m struggling to figure out how to get around something without hacking core.)

The solution I’ve come up with is to basically copy the relevant code from WordPress core and modify it to our needs. Here are the steps (the line numbers are from v3.0.1):

  1. Copy the page_attributes_meta_box() function from line 535 of /wp-admin/includes/meta-boxes.php and modify to suit.

  2. Code an add_meta_boxes hook to add the metabox created in #1.

  3. Copy the get_page_templates() function from line 166 of /wp-admin/includes/theme.php
    and modify to suit.

  4. Copy the page_template_dropdown() function from line 2550 of /wp-admin/includes/template.php and modify to suit.

  5. Add a Post Template to your theme.

  6. Code a save_post hook to enable storing of the post template file name upon save.

  7. Code a single_template hook to enable loading of the post template for the associated posts.

Now on with it!


1. Copy the page_attributes_meta_box() function

As our first step you need to copy the page_attributes_meta_box() function from line 535 of /wp-admin/includes/meta-boxes.php and I’ve chosen to rename it post_template_meta_box(). Since you only asked for page templates I omitted the code for specifying a parent post and for specifying the order which makes the code much simpler. I also chose to use postmeta for this rather than try to reuse the page_template object property in order to avoid and potential incompatibilities caused by unintentional coupling. So here’s the code:

function post_template_meta_box($post) {
  if ( 'post' == $post->post_type && 0 != count( get_post_templates() ) ) {
    $template = get_post_meta($post->ID,'_post_template',true);
    ?>
<label class="screen-reader-text" for="post_template"><?php _e('Post Template') ?></label><select name="post_template" id="post_template">
<option value="default"><?php _e('Default Template'); ?></option>
<?php post_template_dropdown($template); ?>
</select>
<?php
  } ?>
<?php
}

2. Code an add_meta_boxes hook

Next step is to add the metabox using the add_meta_boxes hook:

add_action('add_meta_boxes','add_post_template_metabox');
function add_post_template_metabox() {
    add_meta_box('postparentdiv', __('Post Template'), 'post_template_meta_box', 'post', 'side', 'core');
}

3. Copy the get_page_templates() function

I assumed it would only make sense to differentiate between page templates and post template thus the need for a get_post_templates() function based on get_page_templates() from line 166 of /wp-admin/includes/theme.php. But instead of using the Template Name: marker which page templates use this function uses a Post Template: marker instead which you can see below.

I also filtered out inspection of functions.php (not sure how get_page_templates() ever worked correctly without that, but whatever!) And the only thing left is to change references to the word page to post for maintenance readability down the road:

function get_post_templates() {
  $themes = get_themes();
  $theme = get_current_theme();
  $templates = $themes[$theme]['Template Files'];
  $post_templates = array();

  if ( is_array( $templates ) ) {
    $base = array( trailingslashit(get_template_directory()), trailingslashit(get_stylesheet_directory()) );

    foreach ( $templates as $template ) {
      $basename = str_replace($base, '', $template);
      if ($basename != 'functions.php') {
        // don't allow template files in subdirectories
        if ( false !== strpos($basename, "https://wordpress.stackexchange.com/") )
          continue;

        $template_data = implode( '', file( $template ));

        $name="";
        if ( preg_match( '|Post Template:(.*)$|mi', $template_data, $name ) )
          $name = _cleanup_header_comment($name[1]);

        if ( !empty( $name ) ) {
          $post_templates[trim( $name )] = $basename;
        }
      }
    }
  }

  return $post_templates;
}

4. Copy the page_template_dropdown() function

Similarly copy page_template_dropdown() from line 2550 of /wp-admin/includes/template.php to create post_template_dropdown() and simply change it to call get_post_templates() instead:

function post_template_dropdown( $default="" ) {
  $templates = get_post_templates();
  ksort( $templates );
  foreach (array_keys( $templates ) as $template )
    : if ( $default == $templates[$template] )
      $selected = " selected='selected'";
    else
      $selected = '';
  echo "\n\t<option value="".$templates[$template]."" $selected>$template</option>";
  endforeach;
}

5. Add a Post Template

Next step is to add a post template for testing. Using the Post Template: marker mentioned in step #3 copy single.php from your theme to single-test.php and add the following comment header (be sure to modify something in single-test.php so you can tell it is loading instead of single.php):

/**
 * Post Template: My Test Template
 */

Once you’ve done steps #1 thru #5 you can see your “Post Templates” metabox appear on your post editor page:

What a Post Templates Metabox looked like when added to WordPress 3.0
(source: mikeschinkel.com)

6. Code a save_post hook

Now that you have the editor squared away you need to actually save your page template file name to postmeta when the user clicks “Publish”. Here’s the code for that:

add_action('save_post','save_post_template',10,2);
function save_post_template($post_id,$post) {
  if ($post->post_type=='post' && !empty($_POST['post_template']))
    update_post_meta($post->ID,'_post_template',$_POST['post_template']);
}

7. Code a single_template hook

And lastly you need to actually get WordPress to use your new post templates. You do that by hooking single_template and returning your desired template name for those posts that have had one assigned:

add_filter('single_template','get_post_template_for_template_loader');
function get_post_template_for_template_loader($template) {
  global $wp_query;
  $post = $wp_query->get_queried_object();
  if ($post) {
    $post_template = get_post_meta($post->ID,'_post_template',true);
    if (!empty($post_template) && $post_template!='default')
      $template = get_stylesheet_directory() . "/{$post_template}";
  }
  return $template;
}

And that’s about it!

NOTE that I did not take into consideration Custom Post Types, only post_type=='post'. In my opinion addressing custom post types would require differentiating between the different post types and, while not overly difficult, I didn’t attempt that here.

Leave a Comment