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

FunctionWhat 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; ?>
Note: This is correct, real WordPress template code meant to be pasted into your own theme's template file — this tutorial site doesn't run WordPress, so it isn't executed live on this page.

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:

FunctionBehavior
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.

Rule of thumb: every time you run a secondary WP_Query loop with the_post(), pair it with a wp_reset_postdata() call right after the loop ends.
Next up: Now that you can pull and display content, Theme Development Basics shows you how to package the Loop and its surrounding markup into a real WordPress theme.