Spelunking into the Template Hierarchy

The template hierarchy is one of my favorite features in WordPress. It not only makes child themes possible, but it also makes the whole ecosystem better because more code can be written to sit in smaller and smaller chunks. This is great. What’s also cool about it is that it’s all enabled by a few relatively small chunks of code. But staring at them starts to expose us to some of the most interesting parts of WordPress.

This post assumes some familiarity with PHP, so if you never do it you may get a little turned-around. But it’s not too complicated and heady, so even a novice who sticks with us will certainly learn a thing or two.

A Quick Summary of the Template Hierarchy

This post will not contain an exhaustive summary of the template hierarchy. But some understanding is useful. The template hierarchy basically does two things in WordPress:

  1. It allows for a theme to easily contain many different files for different common cases in WordPress: category archives, single post pages, etc which can each exist independently. The template hierarchy is WordPress’s way of picking among them and making a theme-author’s job a bit easier.
  2. It allows a child theme to step into that same process that the parent theme uses and override individual template choices with its own. This is what makes child themes so powerful: they can quickly change small parts of the parent theme without having to do everything.

It looks a little like this:

wordpress-template-hierarchy

And I always say it, but my favorite place to quickly glance the WordPress template hierarchy is wphierarchy.com.

The Core Function for Child Themes: locate_template

So the template hierarchy requires two core things: that the child theme is checked for files before the parent theme, and that the whole hierarchy proceeds in the way theme authors have been shown it does. Most of the logic that accomplishes the second is in /wp-includes/template-loader.php. More on that later.

Most of the logic that accomplishes the child theme compatibility is in the locate_template function in /wp-includes/template.php. That function looks like this:

function locate_template($template_names, $load = false, $require_once = true ) {
    $located = '';
    foreach ( (array) $template_names as $template_name ) {
        if ( !$template_name )
            continue;
        if ( file_exists(STYLESHEETPATH . '/' . $template_name)) {
            $located = STYLESHEETPATH . '/' . $template_name;
            break;
        } elseif ( file_exists(TEMPLATEPATH . '/' . $template_name) ) {
            $located = TEMPLATEPATH . '/' . $template_name;
            break;
        }
    }

    if ( $load && '' != $located )
        load_template( $located, $require_once );

    return $located;
}

Essentially, locate_template is called with an array of templates it should look for. Its core business is the foreach loop, where it first looks to assure that the passed element of the array is a string. If it is, it them looks in the child theme folder (STYLESHEETPATH is the child theme filesystem path) for a file of the given name — think “archive.php”. If it finds it, it breaks out of the foreach. That is: it stops looping and moves to the end of the loop. If it doesn’t find it in then looks in the “parent” theme (again, TEMPLATEPATH is the parent theme filesystem path).

That’s all there really is to it. It loops though all the files in a given array, and if it finds no files that match it returns the initially set $located value of an empty string. If it finds one that matches as it walks that list in the child theme or (then) the parent theme, it’ll return the string of the full path to that file. It’ll also, depending on how it’s been called, load the file to save the caller an extra pass to the load_template function themselves.

It’s a pretty simple function, locate_template, and there’s not a whole bunch of global variables in use in it which I like. But even still it’s the entire reason that the template hierarchy — and the related functions like get_template_part() and get_header() — works with child themes. Pretty neat!

The Actual Template Hierarchy in Code

It wasn’t until this week in researching this article that I’d actually taken the time to track down the template hierarchy itself inside of WordPress. I think it speaks well of the projects robustness that I’d never had the need to find that code. I think it speaks poorly of my curiosity though.

In any case, how does the template hierarchy get walked through so that a request for a blog post stops at single.php if the theme(s) have it, but progresses all the way to index.php if they don’t?

Basically all requests to a WordPress site go through the index.php file in the project root. That contains the lines:

define('WP_USE_THEMES', true);

/** Loads the WordPress Environment and Template */
require( dirname( __FILE__ ) . '/wp-blog-header.php' );

That pull in wp-blog-header.php. And wp-blog-header.php contains these times:

if ( !isset($wp_did_header) ) {

    $wp_did_header = true;

    require_once( dirname(__FILE__) . '/wp-load.php' );

    wp();

    require_once( ABSPATH . WPINC . '/template-loader.php' );

}

The one that’s interesting to us is the last line: require_once( ABSPATH . WPINC . '/template-loader.php' );. That’s the file that then contains the template hierarchy if a theme will be used to show the page. That file is too big to show in full, but the core part that’s interesting to us is lines 58-91, which say:

if ( defined('WP_USE_THEMES') && WP_USE_THEMES ) :
    $template = false;
    if     ( is_404()            && $template = get_404_template()            ) :
    elseif ( is_search()         && $template = get_search_template()         ) :
    elseif ( is_front_page()     && $template = get_front_page_template()     ) :
    elseif ( is_home()           && $template = get_home_template()           ) :
    elseif ( is_post_type_archive() && $template = get_post_type_archive_template() ) :
    elseif ( is_tax()            && $template = get_taxonomy_template()       ) :
    elseif ( is_attachment()     && $template = get_attachment_template()     ) :
        remove_filter('the_content', 'prepend_attachment');
    elseif ( is_single()         && $template = get_single_template()         ) :
    elseif ( is_page()           && $template = get_page_template()           ) :
    elseif ( is_singular()       && $template = get_singular_template()       ) :
    elseif ( is_category()       && $template = get_category_template()       ) :
    elseif ( is_tag()            && $template = get_tag_template()            ) :
    elseif ( is_author()         && $template = get_author_template()         ) :
    elseif ( is_date()           && $template = get_date_template()           ) :
    elseif ( is_archive()        && $template = get_archive_template()        ) :
    elseif ( is_comments_popup() && $template = get_comments_popup_template() ) :
    elseif ( is_paged()          && $template = get_paged_template()          ) :
    else :
        $template = get_index_template();
    endif;
    /**
     * Filter the path of the current template before including it.
     *
     * @since 3.0.0
     *
     * @param string $template The path of the template to include.
     */
    if ( $template = apply_filters( 'template_include', $template ) )
        include( $template );
    return;
endif;

One giant if block, followed by a simple PHP include call. That is the template hierarchy in all it’s simple glory! How does this whole thing work?

Most WordPress developers will have some experience with the functions like is_404(). They answer whether or not the current page displayed is of that type. That’s all been set up before here, so we can treat it as a magic black-box that those work. So those tags are used in concert with the template = get_404_template()-like calls. And those, under the hood, almost all call locate_template. In the case of 404s, the function looks like:

function get_404_template() {
    return get_query_template('404');
}

For its part, get_query_template is little more than a wrapper to take the passed '404' string and change it into 404.php and then call locate_template with that as the array. (get_query_template also does some basic sanitization, and has an apply_filters call in it, but that’s it really.

So how does the template hierarchy get walked? By a giant if block. If there’s no 404 template file, it’ll go onto the next branch condition and so on at length. Nothing too complicated. And if none of them are found the result of get_index_template() — which gets index.php — is the final resort, as we know of the template hierarchy. Pretty neat, no?

What We Learned About the Template Hierarchy

Hopefully, we’ve demonstrated that a few simple programming features: a big if-elseif-else tree and a loop with some basic file_exists conditionals is sufficient to make the entire template hierarchy run.

My hope is that that knowledge makes you more comfortable diving deep into WordPress and some of it’s core technologies. It’s not the case that the template hierarchy is too esoteric for most developers, though they may initially think so. Most programming is not complicated because of esoterica, but just because you aren’t familiar with where and how the logic is arranged. When that knowledge becomes lived confidence whole new levels of understanding open up for you. Happy hacking!

Image credit: Evan Kirby via Unsplash