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

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, '/') )
          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.