Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 25 of 45

The Loop

How the WordPress Loop works in classic themes, with have_posts(), the_post(), template tags, and what the global post object holds.

13 minClassic templates and queriesUpdated 23 October 2026

By the end of this lessonWrite a Loop that lists posts or shows one, use the common template tags inside it, and handle the case where nothing matches.

What the Loop is

When someone visits /, /category/news/ or /hello-world/, WordPress works out which posts that address needs and runs a database query for them. That is the main query. By the time your template file is loaded, the posts are already waiting. The Loop is the few lines of PHP that go through them one at a time.

This lesson uses a small classic theme. First the theme files that stay the same:

css
/*
Theme Name: Loop Demo
Version: 1.0.0
*/
php
<!doctype html>
<html <?php language_attributes(); ?>>
<head><meta charset="<?php bloginfo( 'charset' ); ?>"><?php wp_head(); ?></head>
<body <?php body_class(); ?>>
php
<?php wp_footer(); ?>
</body>
</html>

The basic Loop

php
<?php get_header(); ?>
<main>
<?php if ( have_posts() ) : ?>
	<?php while ( have_posts() ) : the_post(); ?>
		<article>
			<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
			<p class="date"><?php the_date( 'j F Y' ); ?></p>
		</article>
	<?php endwhile; ?>
<?php else : ?>
	<p class="empty">Nothing found.</p>
<?php endif; ?>
</main>
<?php get_footer(); ?>
bash
wp theme activate loop-demo
result
Success: Switched to 'Loop Demo' theme.

Read it from the inside out:

  • have_posts() is true while there are posts left to show.
  • the_post() takes the next post and makes it the current post. This is what makes the "template tags" work: the_title(), the_permalink() and the_date() have no arguments because they read the current post.
  • When the Loop ends, there are no posts left, and the else branch runs only if there were none at all.

The home page shows the one sample post:

http
GET /
result
Hello world!

Template tags: print or return

Most template tags come in two forms. the_title() prints; get_the_title() returns the value so you can use it in PHP. The same goes for the_permalink() and get_permalink(), the_ID() and get_the_ID().

php
<?php
global $post;
$post = get_post( 1 );
setup_postdata( $post );   // what the_post() does for each post
echo get_the_title(), "\n";
echo get_permalink(), "\n";
echo get_the_date( 'Y-m-d' ), "\n";
echo get_the_author(), "\n";
echo wp_trim_words( get_the_content(), 5, '...' ), "\n";
wp_reset_postdata();
result
Hello world!
http://localhost:8080/hello-world/
2026-01-15
admin
Welcome to WordPress. This is...

Useful tags inside the Loop:

Tag Prints
the_title() the title
the_content() the full content, with blocks rendered and shortcodes run
the_excerpt() the excerpt, or a trimmed start of the content
the_permalink() the post's address
the_date() / the_time() the publication date or time
the_author() the author's display name
the_post_thumbnail() the featured image
the_category() / the_tags() the categories or tags as links

Anything printed from user content is already prepared for output by WordPress. When you print values you built yourself, escape them (see Writing secure WordPress code).

Different template, same Loop

A single post template shows one post, but the Loop is the same, and this one prints the whole content:

php
<?php get_header(); ?>
<main>
<?php while ( have_posts() ) : the_post(); ?>
	<article>
		<h1><?php the_title(); ?></h1>
		<div class="entry"><?php the_content(); ?></div>
	</article>
<?php endwhile; ?>
</main>
<?php get_footer(); ?>
http
GET /hello-world/
result
Hello world!

Search results and the empty case

The search template uses the same Loop. A query that matches nothing shows the else branch of index.php:

http
GET /?s=zebra
result
Nothing found.
http
GET /?s=welcome
result
Hello world!

Never replace the main query yourself

A common beginner mistake is to call query_posts() or to create a new query at the top of a template to change what a page shows. It runs the database query twice and breaks pagination. To change the main query, use the pre_get_posts hook (covered in Custom queries with WP_Query); for an extra list somewhere on a page, make a separate WP_Query.

Common mistakes

  • Calling template tags outside the Loop without a post set up; they print nothing or the wrong post.
  • Forgetting the_post(), which gives an endless loop or the same post repeated.
  • Using echo the_title();. the_title() already prints.
  • No else branch, so an empty search shows a blank page.
  • Using query_posts().

Exercise

Change index.php so that it prints the number of posts found above the list, for example "1 post found". Hint: the main query is in the global $wp_query, and $wp_query->found_posts holds the total.

Show one possible answer
php
<?php global $wp_query; ?>
<p class="count"><?php echo esc_html( sprintf( _n( '%d post found', '%d posts found', $wp_query->found_posts ), $wp_query->found_posts ) ); ?></p>

_n() chooses the singular or plural text. Put the line inside <main>, before the Loop.

Key points

  • Before your template runs, WordPress has already run the main query; the Loop walks through the posts it found.
  • while ( have_posts() ) : the_post(); ... endwhile; makes each post the current one, so template tags such as the_title() and the_content() work.
  • Use the_title() and the_permalink() to print, get_the_title() and get_permalink() to return a value.
  • Always handle the empty case with if ( have_posts() ) ... else ..., and never write your own query for the main content of a template.

Check yourself

1. What does the_post() do inside the Loop?
2. Which function returns the title instead of printing it?
3. Where does the posts the Loop shows come from on a normal page request?
4. How do you show a message when there are no posts?