Introduction
Every time a visitor loads a WordPress page, the same pattern runs behind the scenes. WordPress runs a database query, retrieves a set of posts, and iterates over them — printing each one using your template. That iteration pattern is the WordPress Loop.
Understanding the Loop is one of the most important steps in WordPress development. Once you grasp it, template files stop looking mysterious. You will know why template tags like the_title() and the_content() work where they do, and why they fail outside that context.
This article explains what the Loop is, how it works under the hood, and what you need to know to use it correctly in templates and plugins.
What Is the WordPress Loop?
The WordPress Loop is a PHP while loop that iterates over a set of posts returned by a database query. For each post, it sets up global state, allowing template tags to output data for that post without you needing to pass any arguments.
Here is the most basic version of the Loop:
<?php
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
// Output post data here
the_title( '<h2>', '</h2>' );
the_content();
}
}
That is the entire pattern. Three lines of logic — a check, a loop, and a setup call — power every standard WordPress template.
How the Loop Works
To understand the Loop, you need to understand two functions: have_posts() and the_post().
have_posts()
have_posts() checks whether the current query has any remaining posts to iterate over. It returns true if there are posts left, and false when the list is exhausted. Each call to the_post() advances an internal pointer, and have_posts() reads that pointer to decide what to return next.
The outer if check handles the "no results" case. Without it, the while block is skipped silently. Adding an else clause lets you display a "nothing found" message instead.
<?php
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
// Post output
}
} else {
echo '<p>No posts found.</p>';
}
the_post()
the_post() does the real work. It advances the query's internal post pointer and, critically, calls setup_postdata(). This populates several globals — $post, $id, $authordata, $currentday, and others — with the data for the current post in the iteration.
After the_post() runs, every template tag that reads from these globals now returns data for the correct post. That is why the_title() and the_content() work without arguments inside the Loop — they read the global state that the_post() just set up.
Template Tags Inside the Loop
WordPress ships dozens of template tags designed to run inside the Loop. They all read from the same global state. Here are the ones you will reach for most often:
- the_title() — Outputs the post title
- the_content() — Outputs the full post content, applying filters
- the_excerpt() — Outputs the post excerpt (auto-generated or manual)
- the_ID() — Outputs the post ID
- the_permalink() — Outputs the full URL of the post
- the_date() — Outputs the post publication date
- the_author() — Outputs the post author display name
- the_post_thumbnail() — Outputs the featured image HTML
- the_category() — Outputs a list of categories
- the_tags() — Outputs a list of tags
the_title() vs get_the_title()
Most template tags come in two forms: one that echoes directly to the page, and one that returns a string. the_title() echoes. get_the_title() returns.
Use the get_ version whenever you need the value in a variable, a conditional, or an HTML attribute.
<?php
// Echo directly
the_title( '<h2 class="post-title">', '</h2>' );
// Use in an attribute — escape the value
$title = get_the_title();
echo '<a href="' . esc_url( get_the_permalink() ) . '"
aria-label="Read more about ' . esc_attr( $title ) . '">
Read more
</a>';
Always escape output correctly: esc_html() for display text, esc_attr() for attributes, esc_url() for URLs.
The Main Query and the Main Loop
When WordPress processes a request, it builds the main query automatically based on the URL. For a blog archive, it queries the most recent posts. For a category page, it queries posts in that category. For a single post URL, it queries just that post.
The Loop in your template files — index.php, single.php, archive.php — runs against this main query by default. You do not need to set up a query yourself. WordPress has already done it by the time your template file runs.
This is why a simple index.php file with just the basic Loop structure automatically shows the right posts for every type of page. The URL determines the query; the Loop iterates over the results.
Multiple Loops and Custom Queries
Sometimes you need a second set of posts on the same page — for example, showing related posts below a single post, or displaying featured items in a sidebar. Running the main Loop twice or interrupting it mid-way corrupts WordPress's internal state.
The correct approach is to create a separate query using WP_Query, run the Loop against it, then reset the global post state with wp_reset_postdata().
<?php
$related = new WP_Query( [
'category_name' => 'news',
'posts_per_page' => 3,
] );
if ( $related->have_posts() ) {
while ( $related->have_posts() ) {
$related->the_post();
the_title( '<h3>', '</h3>' );
}
wp_reset_postdata();
}
Why wp_reset_postdata() Matters
When you call $related->the_post(), it changes the global $post to point at a post from the custom query. Any template tags called after this point — even outside your custom Loop — will use that post.
wp_reset_postdata() restores the global $post to the post that was active before your custom query ran. Always call it after every custom Loop. Forgetting it causes subtle, hard-to-trace bugs where template tags return data from the wrong post.
<?php
$custom_query = new WP_Query( $args );
if ( $custom_query->have_posts() ) {
while ( $custom_query->have_posts() ) {
$custom_query->the_post();
the_title();
}
}
wp_reset_postdata(); // Always reset after a custom Loop
The Loop in Different Template Files
The Loop appears in nearly every template file in a classic WordPress theme. The structure is always the same, but the template tags you use inside it vary by context.
On an archive page, you typically output a list of post summaries:
<?php
if ( have_posts() ) :
while ( have_posts() ) :
the_post(); ?>
<article>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<?php the_excerpt(); ?>
</article>
<?php endwhile;
endif;
On a single post page, you output the full content:
<?php
if ( have_posts() ) :
while ( have_posts() ) :
the_post(); ?>
<article>
<h1><?php the_title(); ?></h1>
<?php the_content(); ?>
</article>
<?php endwhile;
endif;
The alternate syntax using colons and endwhile/endif keywords is common in templates because it is easier to read when mixed with HTML blocks.
Best Practices
- Always include the outer if ( have_posts() ) check so empty results are handled gracefully.
- Call wp_reset_postdata() after every custom WP_Query Loop, without exception.
- Use get_ prefixed template tags (get_the_title(), get_the_permalink()) when the value goes into a variable, conditional, or HTML attribute.
- Do not call template tags outside the Loop. They will return empty or incorrect data.
- Never use query_posts() to add a second Loop. It replaces the main query and breaks pagination. Use WP_Query instead.
- Keep business logic out of the Loop. Prepare data before it and reference it inside.
Common Mistakes
Calling Template Tags Outside the Loop
A common mistake is calling the_title() or the_content() in a location where the Loop has not run — such as in a widget callback or a function hooked early in the request. The global $post is not set up at that point, and the tag returns an empty string or data from the wrong post.
If you need post data outside the Loop, fetch it explicitly with get_post( $post_id ) and pass the ID to the relevant get_ function.
You can also guard code that may run in either context. in_the_loop() returns true only while the main Loop is iterating, so wrapping Loop-dependent template tags in it prevents them from firing in the wrong place:
<?php
if ( in_the_loop() ) {
the_title();
}
Forgetting wp_reset_postdata()
Running a custom WP_Query Loop without resetting post data afterwards is one of the most common sources of template bugs. After the Loop, the global $post still points at the last post in the custom query. Template tags called later in the template will silently return data from that post instead of the main post.
Using query_posts() for a Second Loop
Some older tutorials use query_posts() inside a template to run a different Loop. This replaces the main query entirely, breaking pagination and confusing WordPress's routing logic. Always use WP_Query for secondary loops.
Troubleshooting
The Loop Outputs Nothing
Likely causes:
- No posts match the current query (archive with no published posts)
- A plugin or theme is resetting the main query before your template runs
- The template file is running in a context where have_posts() was already exhausted
To diagnose, add var_dump( $wp_query->found_posts ); before the Loop to see how many posts the current query found.
Template Tags Return the Wrong Post
Likely causes:
- A custom WP_Query Loop ran earlier without calling wp_reset_postdata()
- Template tags are being called outside the Loop
To diagnose, add echo get_the_ID(); inside your Loop to confirm which post is active at each iteration.
Pagination Does Not Work
If posts display correctly but pagination links lead to empty pages or the same page, the most common cause is a custom WP_Query that does not pass the current paged value.
<?php
$args = [
'post_type' => 'post',
'posts_per_page' => 10,
'paged' => get_query_var( 'paged' ) ?: 1,
];
That snippet fixes a custom WP_Query. The main query is different: its pagination is handled for you, so render it with the_posts_pagination() or paginate_links() and do not rebuild the query in the template. One more gotcha: on a static front page WordPress uses the page query var instead of paged, so read both with max( 1, get_query_var( 'paged' ), get_query_var( 'page' ) ) when a custom query runs there.
FAQ
Can I use the Loop outside a template file?
Yes. Create a WP_Query instance, call the_post() inside the while loop, and call wp_reset_postdata() afterwards. Many plugins use this pattern to render post data inside shortcodes or widget callbacks.
Does the Loop work with custom post types?
Yes. The Loop does not care about post type. As long as the query returns results — whether posts, pages, or custom post types — the Loop works identically. Template tags like the_title() and the_content() work for any post type.
What is the difference between the Loop and WP_Query?
WP_Query is the class that runs the database query and stores the results. The Loop is the PHP pattern that iterates over those results. The default template Loop runs against the main WP_Query instance built from the current URL. A custom Loop uses a WP_Query instance you create yourself.
When should I use get_posts() instead of WP_Query?
get_posts() is a simpler wrapper that returns an array of post objects without setting up global state. Use it when you only need post data for processing — not for rendering with template tags. Use WP_Query when you need to run a full Loop with template tags. The trade-offs are covered in the WP_Query reference article.
What does the Loop look like in block themes?
Block themes use the Query Loop block in the Site Editor to render post lists. Under the hood, WordPress still runs the same PHP query-and-iterate mechanism. If you write PHP in a render_callback for a dynamic block, the same Loop rules apply. The editing interface changes; the underlying mechanics do not.
Conclusion
The WordPress Loop is the single most important pattern in theme development. It bridges the gap between a database query and your HTML output by setting up a consistent global context for every post in the result set.
Once you understand what have_posts(), the_post(), and wp_reset_postdata() actually do, template files become straightforward to read and write. You will know exactly why template tags work inside the Loop and how to safely add secondary Loops alongside the main one.
The next article in this cluster builds on this foundation and walks through practical Loop patterns for archives, search results, and custom queries.