Chapter 13 — Front-End Fundamentals

Templating in WordPress

You have a working theme. index.php, page.php, single.php, archive.php, and 404.php are all in place. Each one handles a different type of request. Each one has its own version of the same Loop, the same article markup, the same basic structure.

That repetition is a problem. If you decide to change how post metadata is displayed, you’ll edit the same block of HTML in three different files. Miss one and the site is inconsistent. Template parts solve this.


Template parts

A template part is a reusable chunk of template markup extracted into its own file and included where needed with get_template_part(). It’s the same idea as Sass partials or JavaScript modules: one place for each concern, included everywhere it’s needed.

The convention is to keep template parts in a subdirectory called template-parts/:

bash~/tutorials/
mkdir template-parts
touch template-parts/content.php
touch template-parts/content-single.php
touch template-parts/content-page.php
touch template-parts/content-none.php

The naming convention matters. get_template_part() accepts two arguments: a base path and a suffix. It looks for {base}-{suffix}.php first, then falls back to {base}.php. So get_template_part( 'template-parts/content', 'single' ) looks for template-parts/content-single.php first, then template-parts/content.php if the specific version doesn’t exist. This gives you a default with overrides per context.


Build the content template parts

template-parts/content.php (the default post display, used on archives and the index):

php~/tutorials/
<article id="post-<?php the_ID(); ?>" <?php post_class( 'post-card' ); ?>>

  <header class="entry-header">
    <h2 class="entry-title">
      <a href="<?php the_permalink(); ?>" rel="bookmark">
        <?php the_title(); ?>
      </a>
    </h2>
    <div class="entry-meta">
      <time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>">
        <?php echo esc_html( get_the_date() ); ?>
      </time>
      <span class="byline"><?php the_author(); ?></span>
    </div>
  </header>

  <?php if ( has_post_thumbnail() ) : ?>
    <div class="entry-thumbnail">
      <?php the_post_thumbnail( 'medium' ); ?>
    </div>
  <?php endif; ?>

  <div class="entry-summary">
    <?php the_excerpt(); ?>
  </div>

  <a href="<?php the_permalink(); ?>" class="read-more">
    <?php esc_html_e( 'Read more', 'tutorials-theme' ); ?>
  </a>

</article>

template-parts/content-single.php (the full post):

php~/tutorials/
<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">
      <time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>">
        <?php echo esc_html( get_the_date() ); ?>
      </time>
      <span class="byline">
        <?php
        printf(
          /* translators: %s: author display name */
          esc_html__( 'By %s', 'tutorials-theme' ),
          esc_html( get_the_author() )
        );
        ?>
      </span>
    </div>
  </header>

  <?php if ( has_post_thumbnail() ) : ?>
    <div class="entry-thumbnail">
      <?php the_post_thumbnail( 'large' ); ?>
    </div>
  <?php endif; ?>

  <div class="entry-content">
    <?php
    the_content(
      sprintf(
        '<span class="screen-reader-text">%s</span>',
        esc_html__( 'Continue reading', 'tutorials-theme' )
      )
    );

    wp_link_pages(
      array(
        'before' => '<div class="page-links">',
        'after'  => '</div>',
      )
    );
    ?>
  </div>

  <footer class="entry-footer">
    <?php the_tags( '<div class="tags-links">', ', ', '</div>' ); ?>
  </footer>

</article>

template-parts/content-page.php (a static page):

php~/tutorials/
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>

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

  <div class="entry-content">
    <?php
    the_content();

    wp_link_pages(
      array(
        'before' => '<div class="page-links">',
        'after'  => '</div>',
      )
    );
    ?>
  </div>

</article>

template-parts/content-none.php (no results found):

php~/tutorials/
<section class="no-results">
  <header class="entry-header">
    <h1 class="entry-title">
      <?php esc_html_e( 'Nothing found.', 'tutorials-theme' ); ?>
    </h1>
  </header>
  <div class="entry-content">
    <?php if ( is_search() ) : ?>
      <p><?php esc_html_e( 'No results matched your search. Try different terms.', 'tutorials-theme' ); ?></p>
      <?php get_search_form(); ?>
    <?php else : ?>
      <p><?php esc_html_e( "It looks like nothing was found here.", 'tutorials-theme' ); ?></p>
    <?php endif; ?>
  </div>
</section>

A few things in these files worth noting.

has_post_thumbnail() checks whether a featured image has been set before trying to output it. Always check before you output. Outputting a function that finds nothing can produce empty markup or PHP warnings.

the_post_thumbnail( 'medium' ) outputs the featured image at WordPress’s registered medium size. WordPress generates multiple sizes from each uploaded image. Using a registered size means you’re loading an appropriately sized image, not a full-resolution file scaled down in the browser.

printf() with esc_html__() is the correct pattern when you need to insert a dynamic value into a translated string. The %s placeholder gets replaced with the author name. This keeps the translatable string intact while still escaping the output.

wp_link_pages() outputs pagination for multipage posts, posts that use the <!--nextpage--> tag to split content across pages. It outputs nothing if the post isn’t paginated. Including it costs nothing and handles an edge case correctly.


Refactor the template files

Update each template file to use get_template_part() instead of inline markup.

index.php:

php~/tutorials/
<?php get_header(); ?>

<main id="main-content" class="site-main">
  <div class="wrap">

    <?php if ( have_posts() ) : ?>
      <?php while ( have_posts() ) : the_post(); ?>
        <?php get_template_part( 'template-parts/content', get_post_type() ); ?>
      <?php endwhile; ?>
    <?php else : ?>
      <?php get_template_part( 'template-parts/content', 'none' ); ?>
    <?php endif; ?>

  </div>
</main>

<?php get_footer(); ?>

archive.php:

php~/tutorials/
<?php get_header(); ?>

<main id="main-content" class="site-main">
  <div class="wrap">

    <header class="archive-header">
      <h1 class="archive-title"><?php the_archive_title(); ?></h1>
      <?php the_archive_description( '<div class="archive-description">', '</div>' ); ?>
    </header>

    <?php if ( have_posts() ) : ?>
      <div class="post-list">
        <?php while ( have_posts() ) : the_post(); ?>
          <?php get_template_part( 'template-parts/content', get_post_type() ); ?>
        <?php endwhile; ?>
      </div>
    <?php else : ?>
      <?php get_template_part( 'template-parts/content', 'none' ); ?>
    <?php endif; ?>

  </div>
</main>

<?php get_footer(); ?>

single.php:

php~/tutorials/
<?php get_header(); ?>

<main id="main-content" class="site-main">
  <div class="wrap">
    <?php while ( have_posts() ) : the_post(); ?>
      <?php get_template_part( 'template-parts/content', 'single' ); ?>
    <?php endwhile; ?>
  </div>
</main>

<?php get_footer(); ?>

page.php:

php~/tutorials/
<?php get_header(); ?>

<main id="main-content" class="site-main">
  <div class="wrap">
    <?php while ( have_posts() ) : the_post(); ?>
      <?php get_template_part( 'template-parts/content', 'page' ); ?>
    <?php endwhile; ?>
  </div>
</main>

<?php get_footer(); ?>

The template files are now thin. get_post_type() returns the post type of the current post in the loop. Passing it as the suffix to get_template_part() means WordPress looks for a type-specific part and falls back to content.php if it doesn’t find one.


Conditional tags

WordPress provides a set of functions that return true or false based on what is currently being displayed. You’ll use them to vary markup within templates without duplicating entire files.

The most useful ones:

php~/tutorials/
is_front_page()     // true if showing the static front page
is_home()           // true if showing the blog posts index
is_single()         // true if showing a single post
is_page()           // true if showing a static page
is_archive()        // true if showing any archive
is_category()       // true if showing a category archive
is_tag()            // true if showing a tag archive
is_search()         // true if showing search results
is_404()            // true if showing the 404 template
is_user_logged_in() // true if the current visitor is logged in

Each function can also take arguments. is_page( 'about' ) returns true only on the page with the slug about. is_category( 'news' ) returns true only on the news category archive.

A practical example: showing a different label based on context.

php~/tutorials/
<?php if ( is_archive() ) : ?>
  <h2 class="sidebar-label">
    <?php esc_html_e( 'Browse by category', 'tutorials-theme' ); ?>
  </h2>
<?php elseif ( is_single() ) : ?>
  <h2 class="sidebar-label">
    <?php esc_html_e( 'Related reading', 'tutorials-theme' ); ?>
  </h2>
<?php endif; ?>

front-page.php and home.php

These two template files are frequently confused. The distinction is specific and matters.

home.php is served when WordPress is displaying the blog posts index. If you haven’t changed anything under Settings > Reading, this is the front page of the site. WordPress checks for home.php first, then falls back to index.php.

front-page.php is served when a static page is set as the front page under Settings > Reading. When this setting is active, front-page.php takes over the homepage and home.php handles the posts index at whatever URL you’ve assigned it.

The hierarchy:

  • Static front page set: front-page.phppage.phpindex.php
  • Latest posts as front page: front-page.phphome.phpindex.php

Create both:

bash~/tutorials/
touch front-page.php
touch home.php

front-page.php (a static homepage with a hero and a recent posts preview):

php~/tutorials/
<?php get_header(); ?>

<main id="main-content" class="site-main">

  <section class="hero">
    <div class="wrap">
      <?php while ( have_posts() ) : the_post(); ?>
        <h1 class="hero-title"><?php the_title(); ?></h1>
        <div class="hero-content"><?php the_content(); ?></div>
      <?php endwhile; ?>
    </div>
  </section>

  <?php
  $recent_posts = new WP_Query(
    array(
      'posts_per_page' => 3,
      'post_status'    => 'publish',
    )
  );
  ?>

  <?php if ( $recent_posts->have_posts() ) : ?>
    <section class="recent-posts">
      <div class="wrap">
        <h2><?php esc_html_e( 'Recent posts', 'tutorials-theme' ); ?></h2>
        <div class="post-list">
          <?php while ( $recent_posts->have_posts() ) : $recent_posts->the_post(); ?>
            <?php get_template_part( 'template-parts/content', get_post_type() ); ?>
          <?php endwhile; ?>
        </div>
      </div>
    </section>
    <?php wp_reset_postdata(); ?>
  <?php endif; ?>

</main>

<?php get_footer(); ?>

WP_Query builds a custom database query. After a custom WP_Query loop, always call wp_reset_postdata(). It restores the global $post variable to the current page’s post, which any code after your loop may depend on.

home.php (the blog posts index):

php~/tutorials/
<?php get_header(); ?>

<main id="main-content" class="site-main">
  <div class="wrap">

    <header class="page-header">
      <h1 class="page-title">
        <?php
        if ( is_front_page() && is_home() ) :
          esc_html_e( 'Latest posts', 'tutorials-theme' );
        else :
          single_post_title();
        endif;
        ?>
      </h1>
    </header>

    <?php if ( have_posts() ) : ?>
      <div class="post-list">
        <?php while ( have_posts() ) : the_post(); ?>
          <?php get_template_part( 'template-parts/content', get_post_type() ); ?>
        <?php endwhile; ?>
      </div>
    <?php else : ?>
      <?php get_template_part( 'template-parts/content', 'none' ); ?>
    <?php endif; ?>

  </div>
</main>

<?php get_footer(); ?>

Custom page templates

Sometimes you need a page layout that’s different from the standard page.php. A custom page template lets editors assign a specific layout to any page from the page editor.

A custom page template is a regular PHP file with a comment block at the top:

bash~/tutorials/
touch page-full-width.php
php~/tutorials/
<?php
/**
 * Template Name: Full Width
 * Template Post Type: page
 *
 * @package tutorials-theme
 */

get_header();
?>

<main id="main-content" class="site-main site-main--full">

  <?php while ( have_posts() ) : the_post(); ?>
    <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
      <header class="entry-header wrap">
        <h1 class="entry-title"><?php the_title(); ?></h1>
      </header>
      <div class="entry-content">
        <?php the_content(); ?>
      </div>
    </article>
  <?php endwhile; ?>

</main>

<?php get_footer(); ?>

Template Name: Full Width is what appears in the Page Attributes panel under the Template dropdown in the page editor. Template Post Type: page restricts it to pages. The filename doesn’t need to follow any convention when using the comment block approach. WordPress reads the Template Name, not the filename.


search.php

WordPress generates search results pages automatically. search.php handles them.

bash~/tutorials/
touch search.php
php~/tutorials/
<?php get_header(); ?>

<main id="main-content" class="site-main">
  <div class="wrap">

    <header class="page-header">
      <h1 class="page-title">
        <?php
        printf(
          /* translators: %s: search query */
          esc_html__( 'Search results for: %s', 'tutorials-theme' ),
          '<span>' . esc_html( get_search_query() ) . '</span>'
        );
        ?>
      </h1>
    </header>

    <?php if ( have_posts() ) : ?>
      <div class="post-list">
        <?php while ( have_posts() ) : the_post(); ?>
          <?php get_template_part( 'template-parts/content', get_post_type() ); ?>
        <?php endwhile; ?>
      </div>
    <?php else : ?>
      <?php get_template_part( 'template-parts/content', 'none' ); ?>
    <?php endif; ?>

  </div>
</main>

<?php get_footer(); ?>

get_search_query() returns the current search term, sanitized. The content-none.php template part shows a search form when no results are found, handled by the is_search() check you already wrote.


The full theme file structure

plaintext~/tutorials/
tutorials-theme/
├── style.css
├── index.php
├── front-page.php
├── home.php
├── page.php
├── page-full-width.php
├── single.php
├── archive.php
├── search.php
├── 404.php
├── header.php
├── footer.php
├── functions.php
├── inc/
│   └── setup.php
├── template-parts/
│   ├── content.php
│   ├── content-single.php
│   ├── content-page.php
│   └── content-none.php
├── assets/
│   ├── css/main.css
│   └── js/main.js
└── src/
    ├── scss/
    └── js/

Commit your work

bash~/tutorials/
git add .
git commit -m "Add template parts, conditional templates, and full template set"

What you now know

You can organize template markup into reusable parts, use conditional tags to vary output by context, and create custom page templates that editors can assign to specific pages. The front-page.php vs home.php distinction trips up a lot of developers. You now know exactly when each one fires.

In Chapter 14 you’ll go deeper into functions.php: hooks, filters, and the action system. You’ve been using add_action() since Chapter 12 without a full explanation of what it’s doing. That explanation is next.

Reference