Dissecting WordPress Themes Part 3: Single-Post Hierarchy

In the previous article in this series, Dissecting WordPress Themes Part 2: The Loop, we coded a simple WordPress Loop into our index.php file to retrieve some basic information about each post. We discovered that the Loop retrieves posts in reverse-chronological order (most recent posts first). We found that the number of posts on a page is limited by the Blog pages show at most setting and that we can add page navigation to view older and newer posts. We also hyperlinked the post title so that we could isolate a single post by clicking the title text. The Loop automatically handled this by displaying only the one post selected, however, the format of the post output remained the same as when viewing the list of posts.

On many blog sites, the home page will display a list of posts in abbreviated format, perhaps with just the post title, author name, publication date, and an excerpt. This allows more posts to be viewed on this prime real-estate, giving the reader the opportunity to select an article in order to read the full text on a “single-post” page. On this single-post page, the title and basic information is repeated, along with the full post content and comments. Perhaps the sidebar, along with its multitude of widgets, is suppressed on the single-post page in order to allow for distraction-free reading.

In this article, we’ll look at two approaches to construct a different format for single-post pages. We’ll also examine two related features: post formats and custom post types. Post formats don’t actually participate in the single-post hierarchy, but we can simulate the hierarchy for different formats. Post types, on the other hand, do have direct hierarchy components. Along the way, I’ll demonstrate how to activate the standard post formats and create custom post types in our Trace theme.

Single-Post Hierarchy

One approach to creating a single-post look is to alter the index.php file so that it detects that a single post has been selected and adapts its format accordingly. This can be accomplished with the is_single() conditional tag, which returns true on single-post pages and false on multi-post pages. Another approach is to use the template hierarchy to override index.php with a single.php file. When a multi-post page is selected, index.php will run and when a single-post page is selected single.php will run instead. The decision of which approach to take will largely depend on the magnitude of difference between the two formats and subsequent maintenance considerations. We’ll now examine each of the approaches.

Conditional Tag Approach

This approach will be favored when the difference between the multi-post format and the single-post format is relatively minor. As an example, perhaps your design already has the full post content of the most-recent 10 posts on the home page. Also on the home page you want the post’s title to be linked to a single-post page that has basically the same format, but only shows the one, selected post. On the single-post page, perhaps the only difference is that the post title is not linked (since you’re already viewing the full post) and the post’s title is an <h1> element versus an <h2> element on the home page.

Achieving this relatively minor formatting change can be easily accomplished by coding an if-else statement in index.php based on the is_single() conditional tag. Such a code fragment might look like the following:

if (have_posts()) {   // if there are posts
	while (have_posts()) {   // start the loop
		the_post();
		if (is_single()) {
			// put single-post code here
		} else {
			// put multi-post code here
		}
		// put common code here
	}
} else {
	echo "<p>no posts to display</p>\n";
}

When the user first enters the site, the URL will not specify any particular post and so is_single() will return false. The else part of the if-else statement will execute and format the post title in an <h2> element with a hyperlink. When the reader clicks on one of the post links, the URL will request the selected post using the Permalink format setting. The index.php file will be executed again, but this time is_single() will return true. Thus, the if part of the if-else statement will execute, formatting the post title in an <h1> element with no hyperlink. Additional post elements such as author, publication date, categories, tags, content, etc. common to both single- and multi-post formats would come after the if-else statement.

The important thing to realize is that the Loop iteration itself will control the number of posts displayed on the multi-post page and the selected post on the single-post page. So the same Loop code can be used for both formats. Only the formatting specific to each type of page needs to be included in the if-else statement. The other point to consider is that any arbitrary format difference could be coded in this manner. Indeed, the single- and multi-page formats could be radically different and yet still both use the same index.php file. However, that would place all of the code for both pages in the same file, making maintenance more challenging. WordPress solves this problem with its template hierarchy approach, which I’ll describe next.

Template Hierarchy Approach

This approach takes advantage of one of the signature elements of the WordPress architecture, the template hierarchy. The template hierarchy allows a theme developer to override one template file with another, if it is present in the theme’s directory. In the case of single-post pages, this template file is called single.php. Here’s the way it works:

When the user initially enters the site, the URL will not request any particular post and so index.php will be executed. This is the behavior we have seen already. However, when the user clicks on a post link (or a post URL is entered manually in the browser’s address bar), WordPress will execute the single.php template file if there is one in the theme’s directory. This gives us the opportunity to create our single-post page format in a different file than our multi-post page format, thereby easing our maintenance burden when dealing with radically different page formats.

Let’s make some changes to our Trace theme to see how it works. First, copy the index.php file to a new file called single.php.

Note: If you’re still following along with NetBeans, you can copy index.php by right-clicking on the index.php file and selecting copy from the context menu. Then right-click on the trace directory and select paste from the menu. Rename the index_1.php file to single.php. Double-click the single.php file to open the file in an editor tab.

Next, alter the single.php code to that shown below.

<?php
get_header();
echo "<p>In single.php</p>\n";

if (have_posts()) {   // if there are posts
	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 posts to display</p>\n";
}

get_sidebar();
get_footer();
?>

Note that, in this case, we still want to call get_header(), get_sidebar(), and get_footer(). I changed the trace message to In single.php so we can see what template file is executing. I also changed the table format so that it shows various post attributes, including the post contents. Since we are only showing a single post, I removed the page navigation code as well.

Now, click the Visit Site link in the Administration Screen (or refresh your browser if you are already viewing the home page). You should see the post list as we left it in the previous article.

Trace Theme home page

The Trace theme home page executes index.php showing a simple table of post IDs and titles.

Next, click on one of the post hyperlinks, say Post 4. Notice from the trace message that we are now executing single.php. Also recall that we made no change to index.php at all. The only action we took was to add a new template file to the theme. The WordPress template hierarchy functionality discovered the single.php template file and executed it instead of index.php.

single.php

WordPress detects the single.php template file and uses it instead of index.php.

Click your browser’s Back button to return to index.php. Select another post and notice that single.php is executed whenever a single post is selected and index.php is executed whenever we request the site’s home page.

Post Formats

The Post Format is a categorization mechanism similar to categories and tags. While categories and tags are designed for describing the contents of a post, the post formats are designed primarily for customizing the display. There are nine post formats plus the default post format standard. Although formatting for the post formats is left completely up to the theme, it is likely that themes will implement the formats in a similar way. Thus users can become accustomed to the usage of each format and can have an expectation of how they will be interpreted by various themes.

Post Format Description
Aside The aside post format is used for ancillary notes and often styled without a title.
Audio The audio post format is typically used for embedded audio such as a podcast.
Chat Chat transcripts can be flagged as the chat post format. This might be formatted as a conversation between two or more people, such as an text message conversation.
Gallery A gallery will usually contain a selection of images or, perhaps, a slideshow.
Image This post format is usually a single image. The title of the post could represent the image’s title in the post stream.
Link The link post format is used for a hyperlink. A common format uses the post title as the link text with the actual URL in the excerpt or, perhaps, a custom field. Clicking the title would then navigate to the target site rather than the full-text post.
Quote A quote format might use the post title as the author or source of the quote with the actual quote residing in the post content. The quote might be formatted with a <blockquote> element.
Status This post format is usually a short status update. It might not contain a post title at all and formatting to indicate that it is a note and not a regular post.
Video The video post format is for a single video, either embedded or linked in the post content.

In order to use post formats on your site, your theme must first declare which of the post formats (if any) it supports. You do this by creating a template file called functions.php in your theme directory. You then use the add_theme_support() function, passing the post-formats identifier and an array of post formats that your theme will support. In the code below, I’ve registered a function myThemeSetup() on the after_setup_theme action hook. myThemeSetup() then calls add_theme_support() to add all nine of the possible post formats.

<?php
add_action('after_setup_theme', 'myThemeSetup');
function myThemeSetup() {
	// add post formats support
	add_theme_support('post-formats',
		array('aside', 'audio', 'chat', 'gallery',
			'image', 'link', 'quote', 'status', 'video'));
}
?>

If you now return to the Administration Screen and edit any of the posts, you will see a new box on the right which will allow you to choose the format of the post.

Post Format radio buttons

After activating the post formats, the Edit Post page allows you to select one of the post formats or leave the default of Standard.

In order to demonstrate post formats, let’s add four more posts to the database and assign each to one of the post formats. Use the Edit Post page to edit the six existing posts and the Add Post page to add four more posts as shown in the table below.

Post Title Post Contents Format
Post 1 This is Post 1. Standard
Post 2 This is Post 2. Aside
Post 3 This is Post 3. Audio
Post 4 This is Post 4. Chat
Post 5 This is Post 5. Gallery
Post 6 This is Post 6. Image
Post 7 This is Post 7. Link
Post 8 This is Post 8. Quote
Post 9 This is Post 9. Status
Post 10 This is Post 10. Video

Now that we’ve assigned each post to one of the post formats, we will alter our index.php to display the format. We need to add a new column, Format, to the table and use the get_post_format() call to get the format value.

<?php
get_header();
echo "<p>In index.php</p>\n";

if (have_posts()) {   // if there are posts
	echo "<table>\n";
	echo "<tr><th>ID</th><th>Title</th><th>Format</th></tr>\n";
	while (have_posts()) {   // start the loop
		the_post();
		echo "<tr>";
		echo "<td>", get_the_ID(), "</td>";
		echo "<td><a href='", get_permalink(), "'>",
		     get_the_title(), "</a></td>";
		$postFormat = get_post_format();
		echo "<td>", $postFormat ? $postFormat : 'standard', "</td>";
		echo "</tr>\n";
		}
	echo "</table>\n";
	next_posts_link("Older posts");
	echo "<br/>";
	previous_posts_link("Newer posts");
} else {
	echo "<p>no posts to display</p>\n";
}

get_sidebar();
get_footer();
?>

The get_post_format() function will return the format string of all formats except the standard format. Since standard is the default format, the function returns false in this case. In the index.php I just use the conditional operator to hard-code the value standard in this case. Alter your index.php as above and click Visit Site. You should see a page similar to that below (of course, your IDs will likely be different).

Home page showing post formats

The home page with the get_post_format() call now shows the post format for each post.

As it turns out, there is no template hierarchy support for post formats. So it isn’t possible to use a single-[post format].php or archive-[post format].php to stand in for single.php and archive.php, for example. However, you can use get_post_format() in those template files in order to generate format-specific markup. Another option is to use the get_template_part() function.

Simulating Template Hierarchy Support for Post Formats

If you want to use some template-hierarchy functionality similar to what’s available for custom post types (see the next section), you can simulate it with the get_template_part() call. I’ll demonstrate this with the single-post hierarchy. In your single.php file we will replace the Loop with a call to get_template_part().

First, create a new template file called content.php. Then copy the Loop code from single.php and paste it into content.php. Next, let’s add a trace message at the top and add the post format to the table with a call to get_post_format(). The content.php file should look like this:

<?php
echo "<p>In content.php</p>\n";
if (have_posts()) {   // if there are posts
	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 "<tr><td>Post Format</td>";
		$postFormat = get_post_format();
		echo "<td>", $postFormat ? $postFormat : 'standard', "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no posts to display</p>\n";
}
?>

Now, back in single.php, replace the Loop with a call to get_template_part(). We’ll pass two parameters: the string content and the value returned by get_post_format(). This call will include the file content-[post format].php if it exists, otherwise it will fall back to content.php.

<?php
get_header();
echo "<p>In single.php</p>\n";
get_template_part('content', get_post_format());
get_sidebar();
get_footer();
?>

Now, refresh your browser and click on one of the post titles, say Post 8. You should see the page below. Notice that single.php included content.php and that the post format is quote. Click the Back button and select the other posts. Notice in each case that content.php is included. Take special note of Post 1 with the default post format of standard. Even though get_post_format() returns false for this post, the get_template_part() call still includes content.php since it is the fallback.

single.php includes content.php

The template file single.php includes content.php. Its post format is quote.

Let’s assume that you want to provide special formatting for the quote and gallery post formats, but use the standard formatting for the others. All you need to do is copy content.php to content-quote.php and content-gallery.php and then code your format-specific changes in those template files. In our case, we’ll just change the trace message in each so we can tell it’s working. Here is content-quote.php:

<?php
echo "<p>In content-quote.php</p>\n";
if (have_posts()) {   // if there are posts
	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 "<tr><td>Post Format</td>";
		$postFormat = get_post_format();
		echo "<td>", $postFormat ? $postFormat : 'standard', "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no posts to display</p>\n";
}
?>

Here’s content-gallery.php:

<?php
echo "<p>In content-gallery.php</p>\n";
if (have_posts()) {   // if there are posts
	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 "<tr><td>Post Format</td>";
		$postFormat = get_post_format();
		echo "<td>", $postFormat ? $postFormat : 'standard', "</td></tr>\n";
		}
	echo "</table>\n";
} else {
	echo "<p>no posts to display</p>\n";
}
?>

Refresh the home page again and select each of the posts. Note that all posts now load content.php except for Post 5 and Post 8, which load their respective content-[post format].php files instead.

Custom Post Types

In addition to post formats, WordPress also provides the Custom Post Type. A custom post type allows you to define a whole new content type on the same level as posts and pages. WordPress comes out of the box with the following default post types:

Post Type Description
Post Normal posts created from the New Post page in the Administration Screen are assigned the post post type.
Page Normal pages created from the New Page page in the Administration Screen are assigned the page post type. Pages can be hierarchical but cannot be assigned to categories or tags.
Attachment The attachment post type is assigned to objects that hold information about files uploaded to the Media Library. These objects hold metadata about the attachment including title, description, file size, location in the filesystem, alt text, etc.
Revision The revision post type holds draft versions of unpublished posts and past versions of published posts. The post itself is the parent of its revisions.
Navigation Menu Items from a menu are set as navigation post types.

Unlike post formats, custom post types do provide template hierarchy support for both the single-post and archive hierarchies. For example, a custom post type of my_book can be formatted by single-my_book.php and archive-my_book.php. To demonstrate this, we will first create a new custom post type. This can be done by adding the code below to functions.php.

<?php
add_action('after_setup_theme', 'myThemeSetup');
function myThemeSetup() {
	// add post formats support
	add_theme_support('post-formats',
		array('aside', 'audio', 'chat', 'gallery',
			'image', 'link', 'quote', 'status', 'video'));
}

add_action('init', 'myPostTypes');
function myPostTypes() {
	register_post_type ('my_book',
		array ('labels' => array (
				'name' => 'Books',
				'singular_name' => 'Book'),
			'public' => true,
			'has_archive' => true,
			'rewrite' => array ('slug' => 'books'),
			'supports' => array ('title', 'editor', 'author', 'excerpt',
				'revisions', 'comments', 'post-formats'),
			'taxonomies' => array ('category', 'post_tag')
			));
}
?>

After creating the new my_book post type, return to the Administration Screen. You should now see a new top-level menu labeled Books. Click Books→Add New and add a few books with post formats as shown in the table below.

Book Title Book Contents Format
Book 1 This is Book 1. Standard
Book 2 This is Book 2. Gallery
Book 3 This is Book 3. Quote

Now, click Visit Site. Notice that the added Books are not showing up in the main post stream. However, if you enter the URL http://localhost/lab1/books you will see the Books post stream. The final URL component comes from the rewrite parameter in the register_post_type() call.

Books post stream

The Books post stream can be seen by entering the URL in the browser as shown.

If you want the Books stream to be merged with the rest of your posts you can enter the code below in your functions.php file. Here, we hook a function to the pre_get_posts action hook. This function sets the post types in the main query to post and my_book.

<?php
add_action('after_setup_theme', 'myThemeSetup');
function myThemeSetup() {
	// add post formats support
	add_theme_support('post-formats',
		array('aside', 'audio', 'chat', 'gallery',
			'image', 'link', 'quote', 'status', 'video'));
}

add_action('init', 'myPostTypes');
function myPostTypes() {
	register_post_type ('my_book',
		array ('labels' => array (
				'name' => 'Books',
				'singular_name' => 'Book'),
			'public' => true,
			'has_archive' => true,
			'rewrite' => array ('slug' => 'books'),
			'supports' => array ('title', 'editor', 'author', 'excerpt',
				'revisions', 'comments', 'post-formats'),
			'taxonomies' => array ('category', 'post_tag')
			));
}

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

Returning to the home page we now have all posts and books in the same query. Note that this pushed the three oldest posts on to the next page.

Books stream merged with main posts stream

The Books post stream is now merged with the main post stream on the home page.

If you now click on the Book 1 post, you will navigate to the single.php page, which includes the content.php page (since Book 1 has the standard post format). The Book 2 post will include content-gallery.php and the Book 3 post includes content-quote.php, demonstrating that custom post types can be used in combination with post formats.

If we want to have unique formatting for our Book posts, we can use the single-post template hierarchy. To demonstrate, copy single.php to a new template file called single-my_book.php. Change the trace message to In single-my_book.php.

<?php
get_header();
echo "<p>In single-my_book.php</p>\n";
get_template_part('content', get_post_format());
get_sidebar();
get_footer();
?>

Now, go back to the home page of your site and click on one of the Book titles, say Book 2. You should see the Book 2 page and the trace message indicating that WordPress is using the custom post type template file single-my_book.php instead of single.php. Since Book 2 also has the gallery post format, it gets both treatments.

Post type and post format treatments

Since Book 2 has post type my_book and is using the gallery post format, it gets both treatments.

You may be wondering if the single-[post type].php template works with the default post type as well. Let’s try it with the default post type of post. Copy single.php to single-post.php and change the trace message to In single-post.php as shown below.

<?php
get_header();
echo "<p>In single-post.php</p>\n";
get_template_part('content', get_post_format());
get_sidebar();
get_footer();
?>

Now, from the home page post listing, select one of the posts, say Post 7. Notice that, indeed, single-post.php executes instead of single.php.

single-post.php

The template hierarchy allows selection by the default post type post as well as the custom post types.

As I mentioned earlier the custom post types are also supported in the archive hierarchy. I’ll re-visit that in a future article in the series when we drill in to the archive hierarchies.

In this article we looked at the single-post template hierarchy and discovered that WordPress will use single.php instead of index.php if it is present. We found similar hierarchy support for custom post types where single-[post type].php will override single.php when present. While post formats don’t have direct representation in the template hierarchy, we can use the get_template_part() function to dynamically include specific content based on the post format (although get_template_part() isn’t limited to that role).

We can represent the dynamic action of the single-post template hierarchy with the diagram below. A request for a particular post will use the template file single-post.php if it resides in the theme’s directory. A custom post type will use single-[post type].php as its first choice. Assuming these template files are not present, single.php will be used if present, otherwise index.php will be used.

Single-Post Template Hierarchy

The single-post template hierarchy looks for a specific post type template first, then single.php, and finally index.php.

The attachment page hierarchy also uses single.php as its backup, but since this article is long enough already, we’ll save that one until next time!

References

  • Template Hierarchy. WordPress Codex entry for template hierarchy information including a nice overview graphic.
  • Post Formats. WordPress Codex entry for Post Formats including usage, suggested styling, and related function calls.
  • Post Types. WordPress Codex entry for Post Types including creating custom post types, querying post types, and related function calls.

Speak Your Mind

*