The Loop and template tags

Display posts from the main query in classic PHP templates, and see how block themes do the same job.

  • Intermediate
  • 8 min read
  • Updated 17 Oct 2026

What you will learn

  • Explain the Loop
  • Use template tags
  • Know the difference between the_ and get_the_ functions
  • Recognise how block themes replace the PHP Loop with the Query Loop block

Before you start

By the time a template loads, WordPress has already run the main query and knows which posts to show. The Loop is the code in the template that walks through those posts and prints each one. It is the heart of every classic theme, and understanding it explains how WordPress keeps track of "the current post".

The main query, again

In the request lifecycle lesson you saw that WordPress turns the URL into a query and runs it before choosing a template. The result lives in the global $wp_query object:

URLMain query contains
The blog home pageThe latest posts, one page at a time
A single postThat one post
A category archivePosts in that category
A search results pagePosts matching the search
A page that does not existNo posts, and is_404() is true

The template's job is not to fetch posts. It only has to display whatever the main query found.

The basic Loop

This is a minimal index.php for a classic theme:

PHP
<?php
// wp-content/themes/mytheme/index.php
get_header();
?>

<main id="main">
    <?php if ( have_posts() ) : ?>

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

            <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
                <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
                <?php the_excerpt(); ?>
            </article>

        <?php endwhile; ?>

        <?php the_posts_pagination(); ?>

    <?php else : ?>

        <p><?php esc_html_e( 'Nothing found.', 'mytheme' ); ?></p>

    <?php endif; ?>
</main>

<?php
get_footer();

Step by step:

  1. have_posts() returns true while the main query has posts left.
  2. the_post() moves to the next post and sets it up as the current post: it fills the global $post and the data template tags read from.
  3. Template tags such as the_title() print data about the current post.
  4. When the posts run out, have_posts() returns false and the loop ends.

The if ... else around the loop handles empty results, for example a search with no matches. the_posts_pagination() prints next and previous page links for the main query.

Templates usually use PHP's alternative syntax (if ... : and endif;). It reads more clearly when PHP and HTML are mixed. It behaves exactly like curly braces.

Template tags

Template tags are functions that output or return data about the current post. The most common:

TagOutputs
the_title()The post title
the_permalink()The post URL
the_content()The full content, with blocks rendered and filters applied
the_excerpt()The manual excerpt, or an automatic one
the_ID()The post ID
post_class()A class attribute with useful classes such as type-post
the_post_thumbnail( 'medium' )The featured image, at a registered size
the_author()The author's display name
the_time( get_option( 'date_format' ) )The publish date, in the site's date format
the_category( ', ' )Linked category names
the_tags()Linked tag names

the_ versus get_the_

Most tags come in pairs. the_title() echoes the title. get_the_title() returns it, so you can use it in PHP:

PHP
<?php
// Inside the Loop.
$myplugin_title = get_the_title();

if ( strlen( $myplugin_title ) > 60 ) {
    echo '<span class="long-title">' . esc_html( $myplugin_title ) . '</span>';
}

Pairs you will use often: the_title() and get_the_title(), the_permalink() and get_permalink(), the_excerpt() and get_the_excerpt(), the_content() and get_the_content().

The get_ versions also accept a post ID or object, so they work outside the Loop:

PHP
echo esc_html( get_the_title( 42 ) );
echo esc_url( get_permalink( 42 ) );

Escaping template tags

Template tags that echo are designed for templates, and most run their output through filters such as the_title. For values you print yourself, escape for the context:

  • Inside an attribute, use the_title_attribute() or esc_attr( get_the_title() ).
  • URLs from get_permalink() go through esc_url().
  • the_content() outputs HTML on purpose. Do not wrap it in esc_html().
PHP
<a href="<?php echo esc_url( get_permalink() ); ?>" title="<?php the_title_attribute(); ?>">
    <?php the_title(); ?>
</a>

Conditional tags

Templates often need to know what kind of page they are on. Conditional tags read the main query:

PHP
<?php if ( is_single() ) : ?>
    <?php the_content(); ?>
<?php else : ?>
    <?php the_excerpt(); ?>
<?php endif; ?>

Common ones: is_front_page(), is_home() (the blog posts page), is_single(), is_page(), is_singular( 'myplugin_event' ), is_archive(), is_category( 'news' ), is_search() and is_404(). They only work after the main query has run, so use them on wp or later, never on init.

Template hierarchy and the Loop

The template hierarchy picks the most specific template for the request: single.php for a single post, archive.php for archives, page.php for pages, falling back to index.php. Each of them contains a Loop over the same main query. A single.php simply loops once:

PHP
<?php
// wp-content/themes/mytheme/single.php
get_header();

while ( have_posts() ) :
    the_post();
    ?>
    <article <?php post_class(); ?>>
        <h1><?php the_title(); ?></h1>
        <?php the_post_thumbnail( 'large' ); ?>
        <?php the_content(); ?>
    </article>
    <?php
endwhile;

get_footer();

A second loop in a template

Sometimes a template needs other posts as well, such as "latest news" in a sidebar. Never change the main query for this. Run a separate query with WP_Query and reset afterwards:

PHP
<?php
$myplugin_latest = new WP_Query(
    array(
        'posts_per_page' => 3,
        'no_found_rows'  => true,
    )
);

if ( $myplugin_latest->have_posts() ) :
    echo '<ul>';
    while ( $myplugin_latest->have_posts() ) :
        $myplugin_latest->the_post();
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink() ),
            esc_html( get_the_title() )
        );
    endwhile;
    echo '</ul>';
    wp_reset_postdata();
endif;

$myplugin_latest->the_post() replaces the global $post. wp_reset_postdata() puts it back to the main query's current post, so template tags after this block still show the right post. The WP_Query lesson covers custom queries in depth.

You will see query_posts() in old tutorials. It replaces the main query and breaks pagination and conditional tags. Do not use it. Use WP_Query for extra lists, and the pre_get_posts hook to change the main query.

Block themes: HTML templates and the Query Loop block

Block themes do not use PHP template files. Their templates are HTML files of block markup in a templates folder, such as templates/index.html, and they follow the same template hierarchy. The job of the PHP Loop is done by blocks:

  • The Query Loop block (core/query) either inherits the main query or runs a custom one configured in the editor.
  • Its Post Template block repeats for each post.
  • Blocks such as Post Title, Post Excerpt and Post Featured Image replace template tags.
HTML
<!-- wp:query {"queryId":1,"query":{"inherit":true}} -->
<div class="wp-block-query">
    <!-- wp:post-template -->
        <!-- wp:post-title {"isLink":true} /-->
        <!-- wp:post-excerpt /-->
    <!-- /wp:post-template -->
    <!-- wp:query-pagination /-->
</div>
<!-- /wp:query -->

"inherit":true means "use the main query", exactly like a classic Loop. The main query, conditional tags and hooks such as pre_get_posts work the same in both kinds of theme. Building block themes is covered in the Block Theme Development course.

Summary

  • The main query runs before the template; the Loop only displays its results.
  • have_posts() and the_post() drive the Loop and set the current post.
  • Template tags print data about the current post; get_the_ versions return it.
  • Conditional tags such as is_single() read the main query.
  • Use WP_Query plus wp_reset_postdata() for extra lists, never query_posts().
  • Block themes use HTML templates with the Query Loop block instead of PHP loops.

In the next lesson you will see how posts, meta, terms and options are stored in the database.

Further reading