The Loop
The Loop is the single most important piece of PHP in any WordPress theme. It's the mechanism that takes whatever set of posts WordPress has decided to fetch for the current page — a list of blog posts, a single post, search results, an archive — and actually prints them to the screen. Every theme template file that displays content contains some version of the Loop.
1. The Basic Structure
At its core, the Loop is a PHP while loop wrapped around two special functions:
have_posts(), which checks whether there are more posts left to display, and
the_post(), which advances to the next post and sets up all the global template tags
(the_title(), the_content(), etc.) to refer to it.
<?php if (have_posts()) : while (have_posts()) : the_post(); ?>
<h2><?php the_title(); ?></h2>
<div><?php the_content(); ?></div>
<?php endwhile; endif; ?>
The outer if (have_posts()) check guards against pages with zero results (like an empty
search), and the inner while loop repeats once for every post in the current query, calling
the_post() each time to move the "current post" pointer forward.
2. Key Template Tags Used Inside the Loop
| Function | What it outputs |
|---|---|
the_title() | The post's title |
the_content() | The full post body (parses shortcodes and blocks) |
the_excerpt() | A short auto-generated or manually written summary |
the_permalink() | The post's full URL |
the_date() | The publish date |
the_post_thumbnail() | The post's featured image, if one is set |
3. A Full Worked Example: Titles and Excerpts
Here's a more complete loop, as you'd find in a real index.php or home.php
template, printing a linked title, the date, and an excerpt for each post:
<?php if (have_posts()) : while (have_posts()) : the_post(); ?>
<article>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<p class="meta">Posted on <?php the_date(); ?></p>
<?php the_excerpt(); ?>
</article>
<?php endwhile; else: ?>
<p>No posts were found.</p>
<?php endif; ?>
4. the_title() vs get_the_title(): A Common Beginner Trip-Up
This distinction confuses a lot of newcomers, so it's worth calling out clearly:
| Function | Behavior |
|---|---|
the_title() | Echoes the title directly to the page. Use it when you just want to print the title where you are right now. |
get_the_title() | Returns the title as a string instead of printing it. Use it whenever you need to store it in a variable, pass it to another function, or build a conditional around it. |
// Wrong: this echoes immediately, you can't wrap it in a condition
<?php the_title(); ?>
// Right: capture it first so you can check or modify it
<?php
$title = get_the_title();
if (!empty($title)) {
echo '<h2>' . $title . '</h2>';
}
As a general pattern, almost every core template tag has a matching get_ version — reach
for the get_ variant any time you need the value itself rather than an immediate echo.
5. Modifying the Main Query with pre_get_posts
Sometimes you want to change what the main Loop fetches — say, excluding a category from the
homepage — without writing a whole custom query. The pre_get_posts action lets you hook in
and modify the query object just before it runs:
<?php
function exclude_category_from_home($query) {
if ($query->is_main_query() && $query->is_home()) {
$query->set('category__not_in', [7]);
}
}
add_action('pre_get_posts', 'exclude_category_from_home');
This is an advanced technique covered in depth in the Hooks lesson — for now, just know it exists as the "correct" way to tweak the main query, rather than trying to filter posts after the fact in the template.
6. A Separate Custom Loop: Related Posts with WP_Query
Sometimes you need an entirely independent set of posts alongside the main content — for example, a
"Related Posts" box at the bottom of a single post. For that, you create your own
WP_Query object and run a second, completely separate loop:
<?php
$related = new WP_Query([
'category__in' => wp_get_post_categories(get_the_ID()),
'post__not_in' => [get_the_ID()],
'posts_per_page' => 3,
]);
if ($related->have_posts()) : ?>
<h3>Related Posts</h3>
<ul>
<?php while ($related->have_posts()) : $related->the_post(); ?>
<li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php endwhile; ?>
</ul>
<?php endif;
wp_reset_postdata(); ?>
7. Always Call wp_reset_postdata() Afterward
Notice the wp_reset_postdata() call at the very end of the custom loop above. This step is
easy to forget but important: when you call $related->the_post(), it overwrites the
global $post variable that the main Loop was using. If you don't restore it,
anything printed after your related-posts box — comments, next/previous links, another part of
the template — will still be referring to the last post from your custom query instead of the original
page's post, producing confusing bugs. wp_reset_postdata() restores $post back
to whatever the main query had set it to.
WP_Query loop with the_post(), pair it with a wp_reset_postdata() call right after the loop ends.