The WordPress Theme Single Post, Post Attachment & 404 Templates

You’ve built an index of all your posts, now you need to create a template to frame each piece of content (or missing content) on its own. In this lesson, you’ll create templates for single posts, post attachments, and 404 error pages.

The Template for Templates

The structure of single.php (and almost all the other templates we’ll be creating) is largely the same as index.php. In fact you can think of it as our template-template. Go ahead and add the following to single.php.

<?php
/**
 * The Template for displaying all single posts.
 *
 * @package Shape
 * @since Shape 1.0
 */

get_header(); ?>

		<div id="primary" class="content-area">
			<div id="content" class="site-content" role="main">

			<?php while ( have_posts() ) : the_post(); ?>

				<?php shape_content_nav( 'nav-above' ); ?>

				<?php get_template_part( 'content', 'single' ); ?>

				<?php shape_content_nav( 'nav-below' ); ?>

				<?php
					// If comments are open or we have at least one comment, load up the comment template
					if ( comments_open() || '0' != get_comments_number() )
						comments_template( '', true );
				?>

			<?php endwhile; // end of the loop. ?>

			</div><!-- #content .site-content -->
		</div><!-- #primary .content-area -->

<?php get_sidebar(); ?>
<?php get_footer(); ?>

Notice how we’re calling get_template_part() again:

<?php get_template_part( 'content', 'single' ); ?>

This line means that your theme will search for a file called content-single.php to fill in the Loop, otherwise it will default to content.php. We’ll fill up our content-single.php file in just a bit, but first, let’s go over a few things in single.php.

First, since this is a single post, we’ll need the comments_template(). Take a look at this code in single.php:

<?php
// If comments are open or we have at least one comment, load up the comment template
if ( comments_open() || '0' != get_comments_number() )
     comments_template( '', true );
?>

We’ll only load comments if comments are open and if there is at least one comment. And because we’ll be separating our comments and trackbacks when we come to coding up comments.php, we need to call comments_template() like so: comments_template( '', true );.

Single Post Content

Because there will be some notable differences in the way we format single posts (as opposed to posts on the index page), we’re going to create a separate template file for the single post loop. Open content-single.php, and add the following code.

<?php
/**
 * @package Shape
 * @since Shape 1.0
 */
?>

<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
	<header class="entry-header">
		<h1 class="entry-title"><?php the_title(); ?></h1>

		<div class="entry-meta">
			<?php shape_posted_on(); ?>
		</div><!-- .entry-meta -->
	</header><!-- .entry-header -->

	<div class="entry-content">
		<?php the_content(); ?>
		<?php wp_link_pages( array( 'before' => '<div class="page-links">' . __( 'Pages:', 'shape' ), 'after' => '</div>' ) ); ?>
	</div><!-- .entry-content -->

	<footer class="entry-meta">
		<?php
			/* translators: used between list items, there is a space after the comma */
			$category_list = get_the_category_list( __( ', ', 'shape' ) );

			/* translators: used between list items, there is a space after the comma */
                        $tag_list = get_the_tag_list( '', __( ', ', 'shape' ) );

			if ( ! shape_categorized_blog() ) {
				// This blog only has 1 category so we just need to worry about tags in the meta text
				if ( '' != $tag_list ) {
					$meta_text = __( 'This entry was tagged %2$s. Bookmark the <a href="%3$s" title="Permalink to %4$s" rel="bookmark">permalink</a>.', 'shape' );
				} else {
					$meta_text = __( 'Bookmark the <a href="%3$s" title="Permalink to %4$s" rel="bookmark">permalink</a>.', 'shape' );
				}

			} else {
				// But this blog has loads of categories so we should probably display them here
				if ( '' != $tag_list ) {
					$meta_text = __( 'This entry was posted in %1$s and tagged %2$s. Bookmark the <a href="%3$s" title="Permalink to %4$s" rel="bookmark">permalink</a>.', 'shape' );
				} else {
					$meta_text = __( 'This entry was posted in %1$s. Bookmark the <a href="%3$s" title="Permalink to %4$s" rel="bookmark">permalink</a>.', 'shape' );
				}

			} // end check for categories on this blog

			printf(
				$meta_text,
				$category_list,
				$tag_list,
				get_permalink(),
				the_title_attribute( 'echo=0' )
			);
		?>

		<?php edit_post_link( __( 'Edit', 'shape' ), '<span class="edit-link">', '</span>' ); ?>
	</footer><!-- .entry-meta -->
</article><!-- #post-<?php the_ID(); ?> -->

It looks very similar to content.php, with some differences, which we’ll discuss in the next few sections.

Single Post Content

Unlike content.php, in content-single.php, we don’t have any conditionals that tell us to display excerpts on search result pages — because we’re already on the single post view.

Single Post Meta

The single post meta is almost identical to that in content.php. The main difference is that we don’t need to check whether or not this is a Page.

Notice also that we are not including the comments link, because we have already called up the comments_template() on single.php.

Single Post Navigation

Now, let’s go back to single.php. Notice how, just as we did for index.php, we’re using the shape_content_nav() function to handle navigation for single posts. We added this function to inc/template-tags.php in our lesson on the Index Template.

Here are the contents of that function (just in case you missed that lesson, or don’t feel like opening inc/template-tags.php).

function shape_content_nav( $nav_id ) {
	global $wp_query, $post;

	// Don't print empty markup on single pages if there's nowhere to navigate.
	if ( is_single() ) {
		$previous = ( is_attachment() ) ? get_post( $post->post_parent ) : get_adjacent_post( false, '', true );
		$next = get_adjacent_post( false, '', false );

		if ( ! $next && ! $previous )
			return;
	}

	// Don't print empty markup in archives if there's only one page.
	if ( $wp_query->max_num_pages < 2 && ( is_home() || is_archive() || is_search() ) )
		return;

	$nav_class = 'site-navigation paging-navigation';
	if ( is_single() )
		$nav_class = 'site-navigation post-navigation';

	?>
	<nav role="navigation" id="<?php echo $nav_id; ?>" class="<?php echo $nav_class; ?>">
		<h1 class="assistive-text"><?php _e( 'Post navigation', 'shape' ); ?></h1>

	<?php if ( is_single() ) : // navigation links for single posts ?>

		<?php previous_post_link( '<div class="nav-previous">%link</div>', '<span class="meta-nav">' . _x( '&larr;', 'Previous post link', 'shape' ) . '</span> %title' ); ?>
		<?php next_post_link( '<div class="nav-next">%link</div>', '%title <span class="meta-nav">' . _x( '&rarr;', 'Next post link', 'shape' ) . '</span>' ); ?>

	<?php elseif ( $wp_query->max_num_pages > 1 && ( is_home() || is_archive() || is_search() ) ) : // navigation links for home, archive, and search pages ?>

		<?php if ( get_next_posts_link() ) : ?>
		<div class="nav-previous"><?php next_posts_link( __( '<span class="meta-nav">&larr;</span> Older posts', 'shape' ) ); ?></div>
		<?php endif; ?>

		<?php if ( get_previous_posts_link() ) : ?>
		<div class="nav-next"><?php previous_posts_link( __( 'Newer posts <span class="meta-nav">&rarr;</span>', 'shape' ) ); ?></div>
		<?php endif; ?>

	<?php endif; ?>

	</nav><!-- #<?php echo $nav_id; ?> -->
	<?php
}

Here’s the specific part of the function that handles single post navigation.

	<?php if ( is_single() ) : // navigation links for single posts ?>

		<?php previous_post_link( '<div class="nav-previous">%link</div>', '<span class="meta-nav">' . _x( '&larr;', 'Previous post link', 'shape' ) . '</span> %title' ); ?>
		<?php next_post_link( '<div class="nav-next">%link</div>', '%title <span class="meta-nav">' . _x( '&rarr;', 'Next post link', 'shape' ) . '</span>' ); ?>

Instead of using the poorly named next_posts_link() and previous_posts_link() we’ll be using the mostly accurately named previous_post_link() and next_post_link(). They do just what you think they do.

Post Attachments

Not a lot of people use post attachments but they’re kinda interesting. When you add an image to your post you’re actually attaching it to the post. And, of course, you can attach more than just images. We’re going to make an image.php template, but you can, if you like, adapt it further to cover other types of attachments like video, audio, and applications, by making video.php, audio.php, and application.php templates. There’s lots of different ways to be creative with attachment templates and WordPress.

So, without further ado, go ahead and create an image.php file in your theme’s root directory. Once you’ve done that, fill it up with the following.

<?php
/**
 * The template for displaying image attachments.
 *
 * @package Shape
 * @since Shape 1.0
 */

get_header();
?>

		<div id="primary" class="content-area image-attachment">
			<div id="content" class="site-content" role="main">

			<?php while ( have_posts() ) : the_post(); ?>

				<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
					<header class="entry-header">
						<h1 class="entry-title"><?php the_title(); ?></h1>

						<div class="entry-meta">
							<?php
								$metadata = wp_get_attachment_metadata();
								printf( __( 'Published <span class="entry-date"><time class="entry-date" datetime="%1$s" pubdate>%2$s</time></span> at <a href="%3$s" title="Link to full-size image">%4$s &times; %5$s</a> in <a href="%6$s" title="Return to %7$s" rel="gallery">%7$s</a>', 'shape' ),
									esc_attr( get_the_date( 'c' ) ),
									esc_html( get_the_date() ),
									wp_get_attachment_url(),
									$metadata['width'],
									$metadata['height'],
									get_permalink( $post->post_parent ),
									get_the_title( $post->post_parent )
								);
							?>
							<?php edit_post_link( __( 'Edit', 'shape' ), '<span class="sep"> | </span> <span class="edit-link">', '</span>' ); ?>
						</div><!-- .entry-meta -->

						<nav id="image-navigation" class="site-navigation">
							<span class="previous-image"><?php previous_image_link( false, __( '&larr; Previous', 'shape' ) ); ?></span>
							<span class="next-image"><?php next_image_link( false, __( 'Next &rarr;', 'shape' ) ); ?></span>
						</nav><!-- #image-navigation -->
					</header><!-- .entry-header -->

					<div class="entry-content">

						<div class="entry-attachment">
							<div class="attachment">
								<?php
									/**
									 * Grab the IDs of all the image attachments in a gallery so we can get the URL of the next adjacent image in a gallery,
									 * or the first image (if we're looking at the last image in a gallery), or, in a gallery of one, just the link to that image file
									 */
									$attachments = array_values( get_children( array( 'post_parent' => $post->post_parent, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => 'ASC', 'orderby' => 'menu_order ID' ) ) );
									foreach ( $attachments as $k => $attachment ) {
										if ( $attachment->ID == $post->ID )
											break;
									}
									$k++;
									// If there is more than 1 attachment in a gallery
									if ( count( $attachments ) > 1 ) {
										if ( isset( $attachments[ $k ] ) )
											// get the URL of the next image attachment
											$next_attachment_url = get_attachment_link( $attachments[ $k ]->ID );
										else
											// or get the URL of the first image attachment
											$next_attachment_url = get_attachment_link( $attachments[ 0 ]->ID );
									} else {
										// or, if there's only 1 image, get the URL of the image
										$next_attachment_url = wp_get_attachment_url();
									}
								?>

								<a href="<?php echo $next_attachment_url; ?>" title="<?php echo esc_attr( get_the_title() ); ?>" rel="attachment"><?php
									$attachment_size = apply_filters( 'shape_attachment_size', array( 1200, 1200 ) ); // Filterable image size.
									echo wp_get_attachment_image( $post->ID, $attachment_size );
								?></a>
							</div><!-- .attachment -->

							<?php if ( ! empty( $post->post_excerpt ) ) : ?>
							<div class="entry-caption">
								<?php the_excerpt(); ?>
							</div><!-- .entry-caption -->
							<?php endif; ?>
						</div><!-- .entry-attachment -->

						<?php the_content(); ?>
						<?php wp_link_pages( array( 'before' => '<div class="page-links">' . __( 'Pages:', 'shape' ), 'after' => '</div>' ) ); ?>

					</div><!-- .entry-content -->

					<footer class="entry-meta">
						<?php if ( comments_open() && pings_open() ) : // Comments and trackbacks open ?>
							<?php printf( __( '<a class="comment-link" href="#respond" title="Post a comment">Post a comment</a> or leave a trackback: <a class="trackback-link" href="%s" title="Trackback URL for your post" rel="trackback">Trackback URL</a>.', 'shape' ), get_trackback_url() ); ?>
						<?php elseif ( ! comments_open() && pings_open() ) : // Only trackbacks open ?>
							<?php printf( __( 'Comments are closed, but you can leave a trackback: <a class="trackback-link" href="%s" title="Trackback URL for your post" rel="trackback">Trackback URL</a>.', 'shape' ), get_trackback_url() ); ?>
						<?php elseif ( comments_open() && ! pings_open() ) : // Only comments open ?>
							<?php _e( 'Trackbacks are closed, but you can <a class="comment-link" href="#respond" title="Post a comment">post a comment</a>.', 'shape' ); ?>
						<?php elseif ( ! comments_open() && ! pings_open() ) : // Comments and trackbacks closed ?>
							<?php _e( 'Both comments and trackbacks are currently closed.', 'shape' ); ?>
						<?php endif; ?>
						<?php edit_post_link( __( 'Edit', 'shape' ), ' <span class="edit-link">', '</span>' ); ?>
					</footer><!-- .entry-meta -->
				</article><!-- #post-<?php the_ID(); ?> -->

				<?php comments_template(); ?>

			<?php endwhile; // end of the loop. ?>

			</div><!-- #content .site-content -->
		</div><!-- #primary .content-area .image-attachment -->

<?php get_footer(); ?>

.

The attachment template is very similar in structure to single.php. The file looks busier than single.php because we are not using get_template_part() to call a separate template containing the contents of our Loop. Why did we place the contents of the Loop directly into this file? Because we don’t use this specific code in any other place in the theme. If you were going to reuse this code for other templates, it would make sense to place it in a separate file and call it using get_template_part().

You’ll see that it’s very similar to displaying a regular post, only instead of text, we’re displaying the attached image. Let’s go over a few things. Take a look at this code from image.php.

					<header class="entry-header">
						<h1 class="entry-title"><?php the_title(); ?></h1>

						<div class="entry-meta">
							<?php
								$metadata = wp_get_attachment_metadata();
								printf( __( 'Published <span class="entry-date"><time class="entry-date" datetime="%1$s" pubdate>%2$s</time></span> at <a href="%3$s" title="Link to full-size image">%4$s &times; %5$s</a> in <a href="%6$s" title="Return to %7$s" rel="gallery">%7$s</a>', 'shape' ),
									esc_attr( get_the_date( 'c' ) ),
									esc_html( get_the_date() ),
									wp_get_attachment_url(),
									$metadata['width'],
									$metadata['height'],
									get_permalink( $post->post_parent ),
									get_the_title( $post->post_parent )
								);
							?>
							<?php edit_post_link( __( 'Edit', 'shape' ), '<span class="sep"> | </span> <span class="edit-link">', '</span>' ); ?>
						</div><!-- .entry-meta -->
						<nav id="image-navigation" class="site-navigation">
							<span class="previous-image"><?php previous_image_link( false, __( '&larr; Previous', 'shape' ) ); ?></span>
							<span class="next-image"><?php next_image_link( false, __( 'Next &rarr;', 'shape' ) ); ?></span>
						</nav><!-- #image-navigation -->
					</header><!-- .entry-header -->

In this section, we display the title of the image (this is text you enter in the “Title” field when editing an uploaded image) and the image metadata (publication date, image pixel dimensions, and the title of the post it’s attached to), followed by a link to edit the image.

The image navigation links allow us to navigate through all of the images attached to a particular post. In Setting Up Your Theme Functions, we added the function shape_enhanced_image_navigation() to inc/tweaks.php, which suffixes an anchor (#main) to the ends of the next/previous image navigation links. This lets the browser window stay one place when users click through the images. In the Header Template lesson, we also added some JavaScript that enables users to navigate their images using the left and right arrow keys on their keyboard.

The rest of the code in image.php is well-commented, so that you can get a general idea of what it’s doing. Notice that there is no call to get_sidebar(). This is to allow image attachments the optimum amount of space to look awesome. However, if you’d like a sidebar on your image attachment pages, by all means, feel free to add it.

The 404 Template

A 404 Error is the server code for, “I can’t find this page” and it’s an event you need to take care of in your WordPress Themes. What happens when a link to your blog has a post url typed incorrectly? Or you unpublish a blog post? Your server coughs up a 404 error.

Luckily, WordPress has a template for that. It’s called, 404.php. The technique I stick with for 404 Templates is pretty straightforward but it works. Apologize and include a search form. There might be more creative solutions but none that get out of your visitor’s way faster.

Open up 404.php and add something like this.

<?php
/**
 * The template for displaying 404 pages (Not Found).
 *
 * @package Shape
 * @since Shape 1.0
 */

get_header(); ?>

	<div id="primary" class="content-area">
		<div id="content" class="site-content" role="main">

			<article id="post-0" class="post error404 not-found">
				<header class="entry-header">
					<h1 class="entry-title"><?php _e( 'Oops! That page can&rsquo;t be found.', 'shape' ); ?></h1>
				</header><!-- .entry-header -->

				<div class="entry-content">
					<p><?php _e( 'It looks like nothing was found at this location. Maybe try one of the links below or a search?', 'shape' ); ?></p>

					<?php get_search_form(); ?>

					<?php the_widget( 'WP_Widget_Recent_Posts' ); ?>

					<div class="widget">
						<h2 class="widgettitle"><?php _e( 'Most Used Categories', 'shape' ); ?></h2>
						<ul>
						<?php wp_list_categories( array( 'orderby' => 'count', 'order' => 'DESC', 'show_count' => 1, 'title_li' => '', 'number' => 10 ) ); ?>
						</ul>
					</div><!-- .widget -->

					<?php
					/* translators: %1$s: smilie */
					$archive_content = '<p>' . sprintf( __( 'Try looking in the monthly archives. %1$s', 'shape' ), convert_smilies( ':)' ) ) . '</p>';
					the_widget( 'WP_Widget_Archives', 'dropdown=1', "after_title=</h2>$archive_content" );
					?>

					<?php the_widget( 'WP_Widget_Tag_Cloud' ); ?>

				</div><!-- .entry-content -->
			</article><!-- #post-0 .post .error404 .not-found -->

		</div><!-- #content .site-content -->
	</div><!-- #primary .content-area -->

<?php get_footer(); ?>

We’ve omitted the sidebar in this page as well, but you can always opt to add it in by placing a <get_sidebar(); ?> just above the <get_footer(); ?> call.

How To Create a WordPress Theme

This post is part of the The ThemeShaper WordPress Theme Tutorial: 2nd Edition that will show you how to create a powerful WordPress Theme from scratch. Read it from the beginning and code yourself up something awesome.

  1. WordPress Theme Tutorial Introduction
  2. Developing Theme Sense
  3. Theme Development Tools
  4. Creating a Theme HTML Structure
  5. Template and Directory Structure
  6. Setting Up Your Theme Functions
  7. Secure Your WordPress Theme
  8. The Header Template
  9. The Index Template
  10. The Single Post, Post Attachment, & 404 Templates
  11. The Comments Template
  12. The Search Template & The Page Template
  13. The Archive Template
  14. The Sidebar Template & The Footer Template
  15. Reset-Rebuild Theme CSS & Define Your Layouts
  16. Custom Background & Custom Header
  17. Distributing Your WordPress Theme

26 thoughts on “The WordPress Theme Single Post, Post Attachment & 404 Templates

  1. Pingback: The WordPress Theme Single Post, Post Attachment & 404 Templates

  2. Agnes says:

    I am interested to use image.php to display the attachments with some content.
    However, what I’d really like is to get the list of such pages into my sitemap, and I could not achieve that with existing plugins (one list URL of images,but not URL of image pages)
    So I am thinking to habe a page or achive that would list all URLs of image pages. How would I do that?

  3. Not a lot of people use post attachments but they’re kinda interesting.

    Amen! I wish more users and theme developers would familiarize themselves with that core feature before throwing a bunch of jQuery lightboxes on top of it. It’s a neat, conceptually well designed feature that actually enables you to easily set up accessible galleries with permalinked images, just like the ones you see in big online magazines and newspapers.

    There’s nothing to say against a stylish lightbox script in general, but after having implemented a lot of those in client themes, my personal favorite is a well-coded, good old attachment template.

  4. A quick questions regarding the shape_content_nav function.


    $previous = ( is_attachment() ) ? get_post( $post->post_parent ) : get_adjacent_post( false, '', true );
    $next = get_adjacent_post( false, '', false );

    if ( ! $next && ! $previous )
    return;

    So we are first checking for adjacent posts which return either an empty string of a post object. We then check if $next is not empty and if $previous is not empty. If they are we use return to quit out of the shape_content_nav function?

    I always get confused with the not operator – It’s easy to understand when using it with a comparator operator like != but when using it with a return value I get confused.

    Thank you

  5. Dako says:

    Best tutorial to get anyone started with theming in WordPress at my pace (especially for guys like myself who find online video tutorials a frustratingly slow pace….)

  6. Ben Prudden says:

    Great tutorial, clear and concise.

    Just wanted to note/question the get_template_part in the creation of the single.php: Shouldn’t the parameter be ‘single-content’ instead of ‘single’?

    When I copied the code directly it didn’t display any content when clicking on a single post, but when I changed the parameter to ‘single-content’, everything worked.

  7. I cannot get the code to go to content-single.php at all. Leaving the line as is in the tutorial () takes me to the content.php file. Putting something else like ‘single-content’, ‘content-single’, get_post_format() instead of ‘single’ displays the single post, but does not go into the content-single.php file. What am I missing?

  8. I’m using 3.6.1 and called get_template_part exactly as it shows in the tutorial: With the file name as content-single.php. To note, if I change “‘single'” to “‘page,'” the code correctly navigates to content-page.php. An issue with content-single.php? File permissions or corrupt file perhaps?

  9. I don’t know what I did, but I started completely over and it works now. Oddly, when I added content-single.php back in, I couldn’t get it to go to a single page at all. So, I must have done something odd in content.php or

    Still not sure I get what happened, but I’ll play more and see if I can find out.

  10. kuro says:

    Hi I was wondering, I’ve been playing around the tutorial and I can’t seem to make image.php to work. It’s in the theme’s root folder but for the love of god, it always uses the single.php template instead of image.php. Do you know what I might be doing wrong?

    • What type of page are you using to test whether image.php is being used? Is it a single image attachment page? Is there a link to the page I could have a look at?

  11. What’s happening here?

    apply_filters( ‘shape_attachment_size’, array( 1200, 1200 ) ); // Filterable image size.

    I guess I don’t understand how/where shape_attachment_size filter is defined.

    Also, is there a link to get all these files complete so you can see the theme in action without having to piece them together manually? Apologies if it exits and I missed it.

  12. Worst experience when browsing through tutorial codes. I never understand why such rubbish type of long code line and horizontal browsing is present here on themeshaper. Please do something so that code wrap happens.

  13. I’ve really enjoyed this tutorial, I’ve learned a lot and am still amazed that you guys gave us this for FREE! (I’ve paid for similar tutorials and just learned how to copy and paste.) THANK YOU!

    Unfortunately, I have a couple issues with the image.php file and I’ve searched high and low for answers, but can’t figure it out. I’m using this theme to create multiple pages for image galleries (1 gallery/page for different bodies of work). My issues are:

    1. The thumbnails for a gallery are presented correctly. The 1st image, though has a link for previous which takes you to an image not in that gallery. For the 1st image I don’t want the ‘previous’ link to come up. Then, when you get to the last image of the gallery, if you click on the image it takes you to yet another image in different gallery. I want the last image to loop back to the first image of that gallery.

    2. The return to gallery link works only if 1 gallery has been created. When I created a 2nd gallery on a new page, the return link on the image attachment pages brought me back to the 1st gallery.

    I’m working on this locally so I can’t direct you to my site at this point. I haven’t altered the code from what you have posted here, though.

    Thanks in advance for your help.

  14. mirkostingl says:

    I really enjoy this tutorial, thank you very much for your work.
    In this part of the tutorial i found only one minor fault – in the code for content-single.php in line 28 the code isn’t translateable, I think it should be
    $tag_list = get_the_tag_list( '', __( ', ', 'shape' ) );
    Please correct me if I’m wrong.

Comments are closed.