Dissecting WordPress Themes Part 11: Page Hierarchy

0021-fi-web-page-stack
In this article we’ll explore the template hierarchy for pages. We’ll first create a few pages in our development instance and take a look at the default page display using the template of last resort, index.php. We’ll then begin to layer on templates from the hierarchy, demonstrating how each more specific template overrides its more general parent. We’ll examine the role of custom page templates in the hierarchy by creating one and assigning it to a few of our pages. Finally, we’ll observe how we can use page templates to design pages with both static and dynamic content.

Adding Pages

We’ll begin by adding three pages to our WordPress development instance. Log in to the Administration Screen and select Pages→Add New from the left menu to access the Add New Page screen. Enter the title Page 1 in the title field and This is page 1. as the page content. Click the Publish button.

Add New Page screen

The Add New Page screen is used to create new pages.

Create two additional pages, making sure to click Publish for each. All three pages are shown in the table below.

Title Content
Page 1 This is page 1.
Page 2 This is page 2.
Page 3 This is page 3.

Now, if you navigate to the Pages→All Pages menu item you will notice something peculiar. The three pages we just added don’t show up on the list. Additionally, all of the posts and books are listed instead. This is an artifact of our previous attempt to merge posts and books in prior articles.

All Pages screen

This doesn’t look right. The All Pages screen is showing posts and books but no pages. We need to tweak our post/book merge code in functions.php.

The culprit here is our code that merges the posts and books in functions.php. As shown below, the addMyPostTypesToQuery() function does not distinguish whether it is running in the Administration Screens or on the actual site.

add_action('pre_get_posts', 'addMyPostTypesToQuery');
function addMyPostTypesToQuery($query) {
	if (!is_post_type_archive('my_book') && $query->is_main_query() ) {
		$query->set('post_type', array('post', 'my_book'));
	}
}

By changing the merge condition as shown below, we use the is_admin() tag to tell WordPress to skip the post/book merge function when we are viewing an Admin page.

add_action('pre_get_posts', 'addMyPostTypesToQuery');
function addMyPostTypesToQuery($query) {
	if (!is_admin() && !is_post_type_archive('my_book') && $query->is_main_query() ) {
		$query->set('post_type', array('post', 'my_book'));
	}
}

Once you make this change, refresh the Page→All Pages display and you should see just the three pages you added.

All Pages screen fixed

That’s better. Only pages now display on the All Pages screen.

Keep the functions.php file open as we’ll make another change to this function shortly.

Exercise the Hierarchy

On my system the URL pattern for pages is http://localhost/lab1/[slug] where [slug] is the page slug. Depending upon where you installed your WordPress instance, you may have a slightly different URL. Enter the URL for Page 1 in the address bar of your browser. The URL on my system is http://localhost/lab1/page-1. You should see a page similar to screenshot below.

Page display is incorrect

We were expecting the page URL to show us the requested page. Time to tweak the post/book merge code yet again.

Note that absent any more specific template files, index.php is called into action to display pages just as it is used to display posts. However, the standard Loop does not display the page content as you might expect. It turns out that we still don’t have the condition for our post/book merge code quite right. As it stands now, posts and books will be merged whenever we’re not in an admin screen and we’re not on a book archive page and it is a main query. All of those conditions are true for a page request and so the merge code is executed and we retrieve only posts and books. However, since we’re requesting a page, no records satisfy the query. So we need to add one more condition that will skip the filtering action when we’re trying to retrieve a page. We can use the is_page() tag for this as shown below.

add_action('pre_get_posts', 'addMyPostTypesToQuery');
function addMyPostTypesToQuery($query) {
	if (!is_admin() && !is_page() && !is_post_type_archive('my_book') && $query->is_main_query() ) {
		$query->set('post_type', array('post', 'my_book'));
	}
}

If you now refresh your browser with the request for Page 1, you will see the expected output shown below.

The page display is fixed

The requested page is now displayed. This is the same index.php Loop code that displays posts on single- and multiple-post pages.

The same Loop that we used for posts in previous articles now automatically adjusts to retrieve the requested page instead. However, in most themes index.php is not the best template to use for page display so you can override it with the page.php template. Create a new PHP file called page.php in your theme directory and enter the following code:

<?php
get_header(); 
echo "<p>In page.php</p>\n";
if (have_posts()) {   // if there are pages
	echo "<table>\n";
	echo "<tr><th>Attribute</th><th>Value</th></tr>\n";
	while (have_posts()) {   // start the loop
		the_post();
		echo "<tr><td>ID</td>";
		echo "<td>", get_the_ID(), "</td></tr>\n";
		echo "<tr><td>Title</td>";
		echo "<td>", get_the_title(), "</td></tr>\n";
		echo "<tr><td>Content</td>";
		echo "<td>", get_the_content(), "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no pages to display</p>\n";
}
get_sidebar();
get_footer();
?>

This is a simple Loop that displays the page ID, Title, and Content. A refresh of the page request (http://localhost/lab1/page-1/ on my system) will now use the page.php template as shown below. Check the other two pages to verify that they also use the new template.

page.php template

We can override index.php for page requests by creating the page.php template.

The template hierarchy for pages also provides for specific formatting of individual pages. As with other areas of the template hierarchy, you can override the generic template (page.php in this case) with a more specific template using either the page’s ID or slug. I’ll demonstrate both with our Page 1 page.

Copy the page.php template created earlier to a new template called page-[ID].php where the [ID] corresponds to the ID of Page 1 on your system. Recall that we displayed this value in the page.php Loop code above. On my system the ID of Page 1 is 131 as can be seen in the screenshot above, so I will create a new template called page-131.php. Change the trace message so that you can see the effect of the new template.

<?php
get_header(); 
echo "<p>In page-131.php</p>\n";
if (have_posts()) {   // if there are pages
	echo "<table>\n";
	echo "<tr><th>Attribute</th><th>Value</th></tr>\n";
	while (have_posts()) {   // start the loop
		the_post();
		echo "<tr><td>ID</td>";
		echo "<td>", get_the_ID(), "</td></tr>\n";
		echo "<tr><td>Title</td>";
		echo "<td>", get_the_title(), "</td></tr>\n";
		echo "<tr><td>Content</td>";
		echo "<td>", get_the_content(), "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no pages to display</p>\n";
}
get_sidebar();
get_footer();
?>

Next, refresh your browser to redisplay Page 1. You should see that the new template is used as shown below.

Page id template

We can provide formatting for a particular page using its ID with the page-[id].php template.

To perform page-specific formatting using the page’s slug rather than its ID, the template hierarchy provides for a page-[slug].php template pattern. Recall that the page’s slug is set in the Add New Page screen when the page is created. You can also change it on the Edit Page screen (you may have to show the page slug first with Screen Options). Once again, copy page.php to a new template called page-page-1.php and change the trace message as shown below.

<?php
get_header(); 
echo "<p>In page-page-1.php</p>\n";
if (have_posts()) {   // if there are pages
	echo "<table>\n";
	echo "<tr><th>Attribute</th><th>Value</th></tr>\n";
	while (have_posts()) {   // start the loop
		the_post();
		echo "<tr><td>ID</td>";
		echo "<td>", get_the_ID(), "</td></tr>\n";
		echo "<tr><td>Title</td>";
		echo "<td>", get_the_title(), "</td></tr>\n";
		echo "<tr><td>Content</td>";
		echo "<td>", get_the_content(), "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no pages to display</p>\n";
}
get_sidebar();
get_footer();
?>

Once again, refresh your browser window for Page 1 and observe that the new slug-based template overrides the prior ID-based template.

page-[slug].php template

We can also use the page slug to identify a specific page to target.

Of course, you’re not likely to use both the ID and slug for the same page template but both are available to you to provide specific formatting of a particular page.

Custom Page Templates

While the ability to format a specific page differently from the rest is occasionally useful, WordPress also provides the concept of a Custom Page Template, which allows you to format a group of related pages in the same way. A custom page template is implemented with a template file containing a special comment header with the template’s name. Individual pages can then be assigned to this custom page template on the Add New Page and Edit Page screens.

In order to demonstrate the feature, we’ll once again copy the page.php template to new custom page template file. This file can have any name but it makes sense to use some sort of naming convention that will make it easy to locate your custom page templates in the theme’s directory. You could prefix your custom page template filenames with “page-” (e.g. page-mytemplate.php), however this might conflict with the page-[slug].php template pattern. For this example, I’ll use page_mytemplate.php. Here’s the code:

<?php
/*
 * Template Name: My Template
 */
get_header(); 
echo "<p>In page_mytemplate.php</p>\n";
if (have_posts()) {   // if there are pages
	echo "<table>\n";
	echo "<tr><th>Attribute</th><th>Value</th></tr>\n";
	while (have_posts()) {   // start the loop
		the_post();
		echo "<tr><td>ID</td>";
		echo "<td>", get_the_ID(), "</td></tr>\n";
		echo "<tr><td>Title</td>";
		echo "<td>", get_the_title(), "</td></tr>\n";
		echo "<tr><td>Content</td>";
		echo "<td>", get_the_content(), "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no pages to display</p>\n";
}
get_sidebar();
get_footer();
?>

Notice that I added a comment block at the top of the file with the line Template Name: My Template. This comment block is how WordPress recognizes the file as a custom page template and determines the template’s name. Also note the change in the trace message so we can observe the behavior of the template hierarchy.

Next, we need to assign the custom page template to some of our pages. Let’s start with Page 2. Return to the Administration Screen and edit Page 2. You should now see a new field called Template on the Edit Page screen with a drop-down list. Select the new custom page template My Template as shown below. Click Update.

Custom page template assignment

Custom page templates can be created and assigned to multiple pages. Here, we assign the custom My Template template to Page 2.

Now, navigate to Page 2 by entering its URL in your browser’s address bar. On my system the URL for Page 2 is http://localhost/lab1/page-2/. As you can see from the figure below, the custom page template overrides page.php.

Custom page template in action

The custom page template is used to display pages assigned to the template.

That brings up a question: if there is a page template and a custom page template for the same page, which will win? We can find out by assigning Page 1 to the custom page template also. Edit Page 1 and assign it to My Template as you did with Page 2.

Asisgn custom page template to another page

Here we assign our custom page template to Page 1, which is currently using the page-page-1.php template.

Upon navigating back to Page 1, you see that the custom page template overrides page-page-1.php, which itself overrides page-131.php.

Custom page template overrides other page templates

The custom page template overrides the page-[slug].php template, which itself overrides the page-[id].php template

Check Page 3 to verify that it still uses the base page.php template. We now have enough information to extend our template hierarchy diagram to include the page hierarchy (in purple at the bottom).

WordPress Template Hierarchy

The WordPress Template Hierarchy with the addition of the Page hierarchy discussed in this article.

Posts on Pages

Page templates provide the opportunity to include dynamic elements on the page along with the static content that is typed into the page via the editor. Since the page template is a PHP file, it can generate practically any dynamic content, such as list of posts in a particular category. I’ll demonstrate this concept by creating another custom page template called Page with Posts.

Copy the custom page template created earlier to page_page-with-posts.php. Change the trace message at the top as shown below. The first Loop just displays the page content as we saw earlier. The second Loop displays all posts assigned category cat-a.

<?php
/*
 * Template Name: Page With Posts
 */
get_header(); 
echo "<p>In page_page-with-posts.php</p>\n";
if (have_posts()) {   // if there are pages
	echo "<table>\n";
	echo "<tr><th>Attribute</th><th>Value</th></tr>\n";
	while (have_posts()) {   // start the loop
		the_post();
		echo "<tr><td>ID</td>";
		echo "<td>", get_the_ID(), "</td></tr>\n";
		echo "<tr><td>Title</td>";
		echo "<td>", get_the_title(), "</td></tr>\n";
		echo "<tr><td>Content</td>";
		echo "<td>", get_the_content(), "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no pages to display</p>\n";
}

$args = array(
  'category_name' => 'cat-a'
);

$post_list = new WP_Query($args);

if ($post_list->have_posts()) {   // if there are posts
	echo "<p>Related Posts</p>";
	echo "<table>\n";
	echo "<tr><th>ID</th><th>Title</th></tr>\n";
	while ($post_list->have_posts()) {   // start the loop
		$post_list->the_post();
		echo "<tr>";
		echo "<td>", get_the_ID(), "</td>";
		echo "<td><a href='", get_permalink(), "'>",
				get_the_title(), "</a></td>";
		echo "</tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no posts to display</p>\n";
}

get_sidebar();
get_footer();
?>

We’ll now assign our Page 3 to this new Page With Posts custom page template as shown below. Click Update.

Assign another custom page template to Page 3

Here we assign Page 3 to another custom page template. In this template, we display static and dynamic information.

Viewing Page 3 we see a table with static page content at the top, followed by a table of posts assigned to category cat-a below.

Custom page template displaying static and dynamic sections

The custom page template assigned to this page displays the static page content in the top table along with a query of posts in the bottom table. In this version the category name is hard-coded in the template file.

This custom page template would be even more useful if we didn’t hard-code the category but allowed it to vary on a page-by-page basis. We can accomplish this using custom fields. Custom fields represent metadata that can be assigned to each page in the Admin Screen. We can create a custom field for the category of posts to be displayed and then filter the post list based on that value. Make the changes indicated below to the page_page-with-posts.php custom page template.

<?php
/*
 * Template Name: Page With Posts
 */
get_header(); 
echo "<p>In page_page-with-posts.php</p>\n";
if (have_posts()) {   // if there are pages
	echo "<table>\n";
	echo "<tr><th>Attribute</th><th>Value</th></tr>\n";
	while (have_posts()) {   // start the loop
		the_post();
		echo "<tr><td>ID</td>";
		echo "<td>", get_the_ID(), "</td></tr>\n";
		echo "<tr><td>Title</td>";
		echo "<td>", get_the_title(), "</td></tr>\n";
		echo "<tr><td>Content</td>";
		echo "<td>", get_the_content(), "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no pages to display</p>\n";
}

$category = get_post_meta($posts[0]->ID, 'category', true);
$cat = get_cat_ID($category);

$args = array(
  'category__in' => array($cat)
);

$post_list = new WP_Query($args);

if ($post_list->have_posts()) {   // if there are posts
	echo "<p>Related Posts</p>";
	echo "<table>\n";
	echo "<tr><th>ID</th><th>Title</th></tr>\n";
	while ($post_list->have_posts()) {   // start the loop
		$post_list->the_post();
		echo "<tr>";
		echo "<td>", get_the_ID(), "</td>";
		echo "<td><a href='", get_permalink(), "'>",
				get_the_title(), "</a></td>";
		echo "</tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no posts to display</p>\n";
}

get_sidebar();
get_footer();
?>

Next, we’ll go back to edit our Page 3 and add the new custom field as shown below. You may have to display the Custom Fields section of the page by checking the box in Screen Options. After typing category into the Name field and Cat A into the Value field, click the Add New Custom Field button. Click Update.

Create custom field for a page

We can create a custom field called category and assign it the name of the category to be used for the dynamic post query.

Navigating back to Page 3 shows the same results as the hard-coded version before. However, we can now change the value of the category Custom Field and the page will adjust its list of posts. For example, updating the value to Cat B will result in the page display below.

Post query category determined by value of custom field

If we change the value of the custom field, we affect the returned posts in the bottom table of the display.

In this article we examined the segment of the WordPress template hierarchy related to pages. We created pages in our database and demonstrated the template hierarchy using the page.php, page-[ID].php, and page-[slug].php templates. We then took a look at custom page templates and found that they can be assigned to individual pages in order to provide consistent formatting for a group of common pages. We also found that a custom page template will override any other matching page pattern. Finally, we demonstrated how page templates can be used to create layouts that combine fixed and variable content, optionally using custom fields to determine the displayed data.

In the next article, we’ll take a look at the often confusing home and front-page templates.

Speak Your Mind

*