The Loop

The Loop iterates the posts the query already fetched and sets the globals every template tag reads.

Syntaxif ( 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

Example · php
<?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-<?php the_ID(); ?>">
			<h2>
				<a href="<?php echo esc_url( get_permalink() ); ?>">
					<?php the_title(); ?>
				</a>
			</h2>
			<p><?php echo esc_html( get_the_date() ); ?></p>
			<?php the_excerpt(); ?>
		</article>
		<?php
	endwhile;

	the_posts_pagination();

else :
	?>
	<p><?php esc_html_e( 'No posts found.', 'your-theme' ); ?></p>
	<?php
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.

Example · php
<?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.

Example · php
<?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().

Example · php
<?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

  • Be the first to comment on this lesson.