22

My favorite part of the Wordpress template hierarchy is the ability to quickly create template files for pages by slug, without having to edit the page in Wordpress to select a template.

We can currently do this:

page-{slug}.php

But I would like to be able to do this:

single-{post_type}-{slug}.php

So that, for example, in a post type called review, I could make a template for a post called "My Great Review" at single-review-my-great-review.php

Has anybody set this up before? single-{post_type}-{slug}.php

2
  • Never used such a setup before, but if it's too complicated, why not just make a template file and associate it to the review in question.
    – Shane
    Feb 2, 2012 at 16:53
  • WP 3.4 automatically fetches single-{post_type}-{slug}.php, so upgrading to WP 3.4 is another option.
    – yitwail
    Jul 29, 2012 at 17:56

6 Answers 6

19

A) The Base in Core

As you can see in the Codex Template Hierarchy explanation, single-{$post_type}.php is already supported.


B) Extending the core Hierarchy

Now there're gladly some filters and hooks inside /wp-includes/template-loader.php.

  • do_action('template_redirect');
  • apply_filters( 'template_include', $template )
  • AND: a specific filter inside get_query_template( $type, ... ) named: "$type}_template"

B.1) How it works

  1. Inside the template loader file, the template gets loaded by a query var/wp_query conditional: is_*().
  2. The conditional then triggers (in case of a "single" template): is_single() && $template = get_single_template()
  3. This triggers then get_query_template( $type, $templates ), where $type is single
  4. Then we have the "{$type}_template" filter

C) The solution

As we only want to extend the hierarchy with one template that gets loaded before the actual "single-{$object->post_type}.php" template, we'll intercept the hierarchy and add a new template to the beginning of the templates array.

// Extend the hierarchy
function add_posttype_slug_template( $templates )
{

    $object = get_queried_object();

    // New 
    $templates[] = "single-{$object->post_type}-{$object->post_name}.php";
    // Like in core
    $templates[] = "single-{$object->post_type}.php";
    $templates[] = "single.php";

    return locate_template( $templates );    
}
// Now we add the filter to the appropriate hook
function intercept_template_hierarchy()
{
    add_filter( 'single_template', 'add_posttype_slug_template', 10, 1 );
}
add_action( 'template_redirect', 'intercept_template_hierarchy', 20 );

NOTE: (If you want to use something other than the default objects slug) You'll have to adjust $slug according to your permalink-structure. Just use whatever you need from the global (object) $post.

Trac Tickets

As the above approach is currently not supported (you can only filter the absolute located path this way), here's a list of trac tickets:

12
  • I want to test this out, but it looks like there's something missing in your add_filter line at the end.
    – supertrue
    Feb 2, 2012 at 20:45
  • @supertrue Good catch. :) Found another missing ) inside the filter. Fixed. Maybe you want to exchange the dash with an underline before the slug inside the template. Just to let the suffix stand out better when looking over the templates.
    – kaiser
    Feb 2, 2012 at 22:01
  • Causes this error across site: Warning: array_unshift() [function.array-unshift]: The first argument should be an array in [line containing array_unshift]
    – supertrue
    Feb 4, 2012 at 0:02
  • Ok, but then something else is intercepting the core templates. The function works fine and $templates is an array. See the core functions in this pastebin (no expiration date). Make sure that you test this with an install without plugins and the default theme. Then activate one after each other and see if the error still occures.
    – kaiser
    Feb 4, 2012 at 0:47
  • Yeah, I debugged this an I get the final absolute path of the first found template back as string. I'll have to talk to some core developer about this, before altering the answer. Also: I mixed something up: slug is only available for terms and taxonomies. You should replace $post->post_name with what fits your permalink structure. Currently there's no way to automatically do this for all cases with retrieving and replacing the path depending on your perma struct and rewrite rules. Expect another update.
    – kaiser
    Feb 4, 2012 at 1:19
4

Following the Template Hierarchy image, I don't see such an option.

So heres how i'd go about it:

Solution 1 (Best in my opinion)

Make a template file and associate it to the review

 <?php
 /*
 Template Name: My Great Review
 */
 ?>

Adding the template php file in your theme directory, it would appear as a template option in your post's edit page.

Solution 2

This could probably be achieved using template_redirect hook.

In functions.php file :

 function my_redirect()
 {
      global $post;

      if( get_post_type( $post ) == "my_cpt" && is_single() )
      {
           if( file_exists( get_template_directory() . '/single-my_cpt-' . $post->post_name . '.php' ) )
           {
                include( get_template_directory() . '/single-my_cpt-' . $post->post_name . '.php' );
                exit;
           }
      }
 }
 add_action( 'template_redirect', 'my_redirect' );

EDIT

Added file_exists check

4
  • Why do you exit; there?
    – kaiser
    Feb 2, 2012 at 18:26
  • @kaiser Must have been in whatever tutorial I followed at the time, if it's not necessary i'll remove it.
    – Shane
    Feb 2, 2012 at 19:48
  • 1
    @kaiser: The exit() is necessary to prevent loading the default template.
    – scribu
    Feb 3, 2012 at 0:30
  • Solution 1 will only work for pages, not posts.
    – IXN
    Nov 7, 2015 at 12:18
3

The top answer (from 4 years ago) no longer works, but the WordPress codex has the solution here:

<?php
function add_posttype_slug_template( $single_template )
{
    $object = get_queried_object();
    $single_postType_postName_template = locate_template("single-{$object->post_type}-{$object->post_name}.php");
    if( file_exists( $single_postType_postName_template ) )
    {
        return $single_postType_postName_template;
    } else {
        return $single_template;
    }
}
add_filter( 'single_template', 'add_posttype_slug_template', 10, 1 );
?>
1

Use Page Templates

Another approach for scalability would be to duplicate the page template drop-down functionality on the page post type for your custom post type.

Reusable Code

Duplication in code is not a good practice. Overtime it can cause severe bloat to a codebase when then make it very difficult for a developer to manage. Instead of creating a template for every single slug, you most likely will need a one-to-many template that can be reused instead of one-to-one post-to-template.

The Code

# Define your custom post type string
define('MY_CUSTOM_POST_TYPE', 'my-cpt');

/**
 * Register the meta box
 */
add_action('add_meta_boxes', 'page_templates_dropdown_metabox');
function page_templates_dropdown_metabox(){
    add_meta_box(
        MY_CUSTOM_POST_TYPE.'-page-template',
        __('Template', 'rainbow'),
        'render_page_template_dropdown_metabox',
        MY_CUSTOM_POST_TYPE,
        'side', #I prefer placement under the post actions meta box
        'low'
    );
}

/**
 * Render your metabox - This code is similar to what is rendered on the page post type
 * @return void
 */
function render_page_template_dropdown_metabox(){
    global $post;
    $template = get_post_meta($post->ID, '_wp_page_template', true);
    echo "
        <label class='screen-reader-text' for='page_template'>Page Template</label>
            <select name='_wp_page_template' id='page_template'>
            <option value='default'>Default Template</option>";
            page_template_dropdown($template);
    echo "</select>";
}

/**
 * Save the page template
 * @return void
 */
function save_page_template($post_id){

    # Skip the auto saves
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
        return;
    elseif ( defined( 'DOING_AJAX' ) && DOING_AJAX )
        return;
    elseif ( defined( 'DOING_CRON' ) && DOING_CRON )
        return;

    # Only update the page template meta if we are on our specific post type
    elseif(MY_CUSTOM_POST_TYPE === $_POST['post_type'])
        update_post_meta($post_id, '_wp_page_template', esc_attr($_POST['_wp_page_template']));
}
add_action('save_post', 'save_page_template');


/**
 * Set the page template
 * @param string $template The determined template from the WordPress brain
 * @return string $template Full path to predefined or custom page template
 */
function set_page_template($template){
    global $post;
    if(MY_CUSTOM_POST_TYPE === $post->post_type){
        $custom_template = get_post_meta($post->ID, '_wp_page_template', true);
        if($custom_template)
            #since our dropdown only gives the basename, use the locate_template() function to easily find the full path
            return locate_template($custom_template);
    }
    return $template;
}
add_filter('single_template', 'set_page_template');

This is a bit of a late answer, but I thought it would be valuable since no one on the web has documented this approach as far as I can tell. Hope this helps someone out.

1

In my case, I have Album and Track custom post types linked by an Album taxonomy. I wanted to be able to use different Single templates for the Album and Track posts depending on their Album taxonomy.

Based on Kaiser's answer above, I wrote this code. It works well.
Note. I didn't need the add_action().

// Add an additional template option to the template hierarchy
add_filter( 'single_template', 'add_albumtrack_taxslug_template', 10, 1 );
function add_albumtrack_taxslug_template( $orig_template_path )
{
    // at this point, $orig_template_path is an absolute located path to the preferred single template.

    $object = get_queried_object();

    if ( ! (
        // specify another template option only for Album and Track post types.
        in_array( $object->post_type, array( 'gregory-cpt-album','gregory-cpt-track' )) &&
        // check that the Album taxonomy has been registered.
        taxonomy_exists( 'gregory-tax-album' ) &&
        // get the Album taxonomy term for the current post.
        $album_tax = wp_get_object_terms( $object->ID, 'gregory-tax-album' )
        ))
        return $orig_template_path;

    // assemble template name
    // assumption: only one Album taxonomy term per post. we use the first object in the array.
    $template = "single-{$object->post_type}-{$album_tax[0]->slug}.php";
    $template = locate_template( $template );
    return ( !empty( $template ) ? $template : $orig_template_path );
}

I can now create templates named single-gregory-cpt-track-tax-serendipity.php and single-gregory-cpt-album-tax-serendipity.php and WP will use them automatically; 'tax-serendipity' is the slug for the first Album taxonomy term.

for reference, the 'single_template' filter hook is declared in:
/wp-includes/theme.php: get_query_template()

Thank you Kaiser for the sample code.

Cheers, Gregory

5
  • Hi Greg - welcome to WPSE. Please only post answers as answers to the questions - not follow up questions. If you have a question that isn't answered by an existing answer and is too large for a comment, please open another question :) Apr 18, 2012 at 15:07
  • 1
    the string/array question has been removed :-)
    – Gregory
    Apr 18, 2012 at 16:04
  • 1
    "Thank you Kaiser for the sample code." - You're welcome.
    – kaiser
    Oct 23, 2012 at 13:01
  • does it work for you? first of all, '$template' shouldn't be commented in your code.. and I think that instead of '$album_tax[0]->slug' there should be '$object->post_name', isn't it?
    – gregmatys
    Nov 18, 2013 at 14:14
  • fixed the $template line. thank you. $object->post_name? no. that would return the post's slug, but I need the slug of the album that the post is linked to.
    – Gregory
    Nov 18, 2013 at 23:21
0

Update for Brians code, I found that when the dropdown box was not being used the "default" template option was being saved into wp_page_template which caused it to try and find a template called default. this change just checks for the option "default" when saving and deletes the post meta instead (useful if you changed the template option back to default)

elseif(MY_CUSTOM_POST_TYPE === $_POST['post_type']) {

if ( esc_attr($_POST['_wp_page_template']) === "default" ) :
    delete_post_meta($post_id, '_wp_page_template');
else :
    update_post_meta($post_id, '_wp_page_template', esc_attr($_POST['_wp_page_template']));
endif;
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.