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:
| URL | Main query contains |
|---|---|
| The blog home page | The latest posts, one page at a time |
| A single post | That one post |
| A category archive | Posts in that category |
| A search results page | Posts matching the search |
| A page that does not exist | No 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
// 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:
have_posts()returnstruewhile the main query has posts left.the_post()moves to the next post and sets it up as the current post: it fills the global$postand the data template tags read from.- Template tags such as
the_title()print data about the current post. - When the posts run out,
have_posts()returnsfalseand 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:
| Tag | Outputs |
|---|---|
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
// 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:
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()oresc_attr( get_the_title() ). - URLs from
get_permalink()go throughesc_url(). the_content()outputs HTML on purpose. Do not wrap it inesc_html().
<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 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
// 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
$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.
<!-- 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()andthe_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_Querypluswp_reset_postdata()for extra lists, neverquery_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.