The Loop
The Loop iterates the posts the query already fetched and sets the globals every template tag reads.
if ( have_posts() ) : while ( have_posts() ) : the_post(); /* template tags */ endwhile; endif;The Loop is the standard way a template renders posts WordPress has already fetched. By the time index.php or single.php runs, the main query has executed and its results sit in the global $wp_query. The Loop walks that result set and, for each post, fills in the globals that template tags read.
How it works
have_posts() returns a bool: true while posts remain. the_post() advances the pointer, assigns the global $post, and calls setup_postdata(), which populates $authordata, $page, $pages, $multipage, $more and $numpages. That is why the_title() and the_content() take no post argument — they read the globals the_post() just set.
When have_posts() runs out it fires the loop_end action, calls rewind_posts() and sets in_the_loop to false, so the same query can be looped again without re-querying.
Shorthand and longhand
The global functions are thin wrappers and mean exactly the same thing as the method calls: have_posts() is $wp_query->have_posts(), and the_post() is $wp_query->the_post(). Templates also favour the alternative control syntax, while ( ... ) : ... endwhile;, which is identical to while ( ... ) { ... } — it just reads better when HTML is interleaved.
What trips people up
Leaving out the_post() means the pointer never advances and the request loops forever. Calling a template tag outside the Loop reads whatever $post was last set, which on an archive is the last post, not the current one.
Example
<?php
// wp-content/themes/your-theme/index.php
get_header();
if ( have_posts() ) :
// have_posts() and the_post() are wrappers around the global $wp_query:
// $wp_query->have_posts() and $wp_query->the_post(). Same thing.
while ( have_posts() ) :
the_post(); // advances the pointer, sets global $post, runs setup_postdata()
<article id="post- the_ID(); ">
<h2>
<a href=" echo esc_url( get_permalink() ); ">
the_title();
</a>
</h2>
<p> echo esc_html( get_the_date() ); </p>
the_excerpt();
</article>
endwhile;
the_posts_pagination();
else :
<p> esc_html_e( 'No posts found.', 'your-theme' ); </p>
endif;
get_footer();When to use it
- Render an archive listing in index.php or archive.php
- Show a 'nothing found' message when a search or archive returns no posts
- Output a featured strip and a full listing from a single query
- Read a related-posts WP_Query with the same template tags used in the main loop
More examples
Alternative syntax and braces are the same loop
Core themes and the handbook use the colon/endwhile form, so you must be able to read it, but it desugars to a plain if/while. Reach for the colon form in template files where PHP and HTML alternate, and braces in functions.php where there is no markup.
<?php
// Longhand: ordinary PHP braces.
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
echo '<h2>' . esc_html( get_the_title() ) . '</h2>';
}
}
// Shorthand used by almost every theme: alternative control syntax.
// Identical behaviour; the colon form just closes cleanly around HTML.
if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
<h2><?php the_title(); ?></h2>
<?php endwhile; endif;Looping the same query twice with rewind_posts()
Shows that the Loop is a pointer over an already-fetched array, not a database call: rewinding replays the same rows for free. Use it when one template needs two passes over the same posts, such as a hero block above a full archive listing.
<?php
// A featured strip followed by the full list, from one query.
global $wp_query;
while ( have_posts() ) {
the_post();
if ( $wp_query->current_post >= 3 ) {
break; // only the first three
}
echo '<li>' . esc_html( get_the_title() ) . '</li>';
}
// have_posts() rewinds automatically only when it runs to the end.
// We broke out early, so rewind by hand before looping again.
rewind_posts();
while ( have_posts() ) {
the_post();
echo '<p>' . esc_html( get_the_title() ) . '</p>';
}
wp_reset_postdata();A loop over an explicit WP_Query object
Any WP_Query instance loops the same way; only the receiver changes. Use this shape for sidebars, related-post blocks and anything that is not the page's primary content, and always finish with wp_reset_postdata().
<?php
$related = new WP_Query(
array(
'post_type' => 'post',
'posts_per_page' => 4,
'post__not_in' => array( get_the_ID() ),
)
);
// The object methods are the longhand the global functions wrap.
if ( $related->have_posts() ) {
while ( $related->have_posts() ) {
$related->the_post(); // still sets the same global $post
echo '<a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a>';
}
// Restore the global $post to the main query's current post.
wp_reset_postdata();
}
Discussion