How to move page template files like page-{slug}.php to a sub-directory?

How Page Templates are loaded:

According to the default WordPress Template Hierarchy, a page request loads a template based on the priority and naming as stated below:

  1. Custom Page Template: if defined in the page editor.
  2. page-{slug}.php
  3. page-{url-encoded-slug}.php: only for multi-byte characters.
  4. page-{id}.php
  5. page.php
  6. singular.php
  7. index.php

Among these, singular.php and index.php are not actually page templates. singular.php is the fallback template for any single post types and index.php is the ultimate fallback template for anything that a WordPress template is supposed to load. So the first five are page templates.

How to inject template files from a sub-directory within the hierarchy:

WordPress core function get_page_template() generates the necessary page template hierarchy array and just before deciding exactly which template file to load from the hierarchy, WordPress fires the page_template_hierarchy filter hook. So the best way to add a sub-directory, where WordPress will look for page-{slug}.php templates automatically, is to use this filter and inject proper file names relative to that sub-directory within the page template hierarchy array.

Note: the original filter hook is a dynamic filter hook defined as {$type}_template_hierarchy, which is located in wp-includes/template.php file. So when the $type is page, the filter hook becomes page_template_hierarchy.

Now, for our purpose, we’ll inject the sub-directory/page-{slug}.php file name just before page-{slug}.php within the template hierarchy array passed to the hooks callback function. That way, WordPress will load sub-directory/page-{slug}.php file if it exists, otherwise it’ll follow the normal page template loading hierarchy. Of course, to maintain consistency, we’ll still give Custom Page Template a higher priority compared to our sub-directory/page-{slug}.php file. So the modified page template hierarchy will become:

  1. Custom Page Template: if defined in the page editor.
  2. sub-directory/page-{slug}.php
  3. sub-directory/page-{url-encoded-slug}.php: only for multi-byte characters.
  4. page-{slug}.php
  5. page-{url-encoded-slug}.php: only for multi-byte characters.
  6. page-{id}.php
  7. page.php

Sample functions.php CODE:

If you plan to do this change only on a single theme, then you may use the following CODE in your active theme’s functions.php file:

// defining the sub-directory so that it can be easily accessed from elsewhere as well.
define( 'WPSE_PAGE_TEMPLATE_SUB_DIR', 'page-templates' );

function wpse312159_page_template_add_subdir( $templates = array() ) {
    // Generally this doesn't happen, unless another plugin / theme does modifications
    // of their own. In that case, it's better not to mess with it again with our code.
    if( empty( $templates ) || ! is_array( $templates ) || count( $templates ) < 3 )
        return $templates;

    $page_tpl_idx = 0;
    if( $templates[0] === get_page_template_slug() ) {
        // if there is custom template, then our page-{slug}.php template is at the next index 
        $page_tpl_idx = 1;
    }

    $page_tpls = array( WPSE_PAGE_TEMPLATE_SUB_DIR . "https://wordpress.stackexchange.com/" . $templates[$page_tpl_idx] );

    // As of WordPress 4.7, the URL decoded page-{$slug}.php template file is included in the
    // page template hierarchy just before the URL encoded page-{$slug}.php template file.
    // Also, WordPress always keeps the page id different from page slug. So page-{slug}.php will
    // always be different from page-{id}.php, even if you try to input the {id} as {slug}.
    // So this check will work for WordPress versions prior to 4.7 as well.
    if( $templates[$page_tpl_idx] === urldecode( $templates[$page_tpl_idx + 1] ) ) {
        $page_tpls[] = WPSE_PAGE_TEMPLATE_SUB_DIR . "https://wordpress.stackexchange.com/" . $templates[$page_tpl_idx + 1];
    }

    array_splice( $templates, $page_tpl_idx, 0, $page_tpls );

    return $templates;
}
add_filter( 'page_template_hierarchy', 'wpse312159_page_template_add_subdir' );

Sample Plugin:

If you want to follow the same template file organization in multiple themes, then it’s best to keep this feature separate from your theme. In that case, instead of modifying the theme’s functions.php file with the above sample CODE, you’ll need to create a simple plugin with the same sample CODE.

Save the following CODE with a file name e.g. page-slug-template-subdir.php within your WordPress plugins directory:

<?php
/*
Plugin Name:  WPSE Page Template page-slug.php to Sub Directory
Plugin URI:   https://wordpress.stackexchange.com/a/312159/110572
Description:  Page Template with page-{slug}.php to a Sub Directory
Version:      1.0.0
Author:       Fayaz Ahmed
Author URI:   https://www.fayazmiraz.com/
*/

// defining the sub-directory so that it can be easily accessed from elsewhere as well.
define( 'WPSE_PAGE_TEMPLATE_SUB_DIR', 'page-templates' );

function wpse312159_page_template_add_subdir( $templates = array() ) {
    // Generally this doesn't happen, unless another plugin / theme does modifications
    // of their own. In that case, it's better not to mess with it again with our code.
    if( empty( $templates ) || ! is_array( $templates ) || count( $templates ) < 3 )
        return $templates;

    $page_tpl_idx = 0;
    if( $templates[0] === get_page_template_slug() ) {
        // if there is custom template, then our page-{slug}.php template is at the next index 
        $page_tpl_idx = 1;
    }

    $page_tpls = array( WPSE_PAGE_TEMPLATE_SUB_DIR . "https://wordpress.stackexchange.com/" . $templates[$page_tpl_idx] );
                                                                                  uded in the
    // page template hierarchy just before the URL encoded page-{$slug}.php template file.
    // Also, WordPress always keeps the page id different from page slug. So page-{slug}.php will
    // always be different from page-{id}.php, even if you try to input the {id} as {slug}.
    // So this check will work for WordPress versions prior to 4.7 as well.
    if( $templates[$page_tpl_idx] === urldecode( $templates[$page_tpl_idx + 1] ) ) {
        $page_tpls[] = WPSE_PAGE_TEMPLATE_SUB_DIR . "https://wordpress.stackexchange.com/" . $templates[$page_tpl_idx + 1];
    }

    array_splice( $templates, $page_tpl_idx, 0, $page_tpls );

    return $templates;
}
// the original filter hook is {$type}_template_hierarchy,
// wihch is located in wp-includes/template.php file
add_filter( 'page_template_hierarchy', 'wpse312159_page_template_add_subdir' );

Usage:

With either of the above CODE, WordPress will recognize page-{slug}.php template files within page-templates directory of your theme automatically.

Say for example, you have an about page. So, if it doesn’t have a custom page template set from the editor, then WordPress will look for THEME/page-templates/page-about.php template file and if that doesn’t exist, then WordPress will look for THEME/page-about.php template file and so on (i.e. the default page template hierarchy).

Leave a Comment