Skip to content

PHP Tutorial PHP for WordPress Developers

The WordPress Loop

Lesson 328 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

The Loop is the PHP code that goes through the posts WordPress found for the current request and prints each one.

index.php
PHP
<?php if (have_posts()) : ?>
  <?php while (have_posts()) : the_post(); ?>
    <article>
      <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
      <div><?php the_excerpt(); ?></div>
    </article>
  <?php endwhile; ?>
<?php else : ?>
  <p>No posts found.</p>
<?php endif; ?>

How it works

  • have_posts() asks: are there more posts in the current query?
  • the_post() moves to the next post and sets up its data (the global $post).
  • Template tags (the_title(), the_permalink()) then print data for that post.

It is a while loop, just like PHP while. The “list” is WordPress’s main query, built from the URL before your template runs.

Printing vs returning

PrintsReturns
the_title()get_the_title()
the_permalink()get_permalink()
the_content()get_the_content()
the_excerpt()get_the_excerpt()
the_ID()get_the_ID()

Use the get_ version when you want to change the value or put it in a variable, and escape it yourself.

index.php
PHP
<h2><?php echo esc_html(get_the_title()); ?></h2>
<a href="<?php echo esc_url(get_permalink()); ?>">Read more</a>

A custom loop with WP_Query

To show other posts (the latest five products, posts in a category), create your own query. WP_Query takes an array of arguments, like the arrays you know.

index.php
PHP
<?php
$query = new WP_Query([
    "post_type" => "post",
    "posts_per_page" => 5,
    "post_status" => "publish",
    "category_name" => "news",
    "orderby" => "date",
    "order" => "DESC",
]);

if ($query->have_posts()) :
    while ($query->have_posts()) : $query->the_post();
        ?>
        <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
        <?php
    endwhile;
    wp_reset_postdata();   // IMPORTANT: restore the main loop's $post
endif;

wp_reset_postdata() puts the global $post back to what it was. Forgetting it breaks anything that runs after your loop.

Common WP_Query arguments

ArgumentMeaning
post_typepost, page, or a custom post type
posts_per_pageHow many posts (-1 for all, use with care)
post_statuspublish, draft …
orderby, orderSorting
category_name, tag, tax_queryFilter by taxonomy
meta_key, meta_queryFilter by custom fields
sSearch term
pagedPage number for pagination
post__in, post__not_inInclude or exclude IDs
no_found_rowsSet true when you do not need pagination (faster)

You can test building arguments with plain PHP, because they are only an array:

index.php
PHP
<?php
function latestPostsArgs(string $category = "", int $count = 5): array
{
    $args = ["post_type" => "post", "posts_per_page" => $count, "orderby" => "date", "order" => "DESC"];

    if ($category !== "") {
        $args["category_name"] = $category;
    }

    return $args;
}

echo json_encode(latestPostsArgs("news", 3));
Output
OUTPUT
{"post_type":"post","posts_per_page":3,"orderby":"date","order":"DESC","category_name":"news"}

get_posts() for simple lists

get_posts() returns an array of post objects. It does not touch the main loop.

index.php
PHP
<?php
$posts = get_posts(["numberposts" => 3, "post_type" => "post"]);

foreach ($posts as $post) {
    echo esc_html($post->post_title);
}

Pagination

index.php
PHP
<?php
the_posts_pagination([
    "mid_size" => 2,
    "prev_text" => __("Previous", "my-theme"),
    "next_text" => __("Next", "my-theme"),
]);

For a custom query, pass paged (get_query_var("paged")) and use $query->max_num_pages.

Never change the main query with a new WP_Query

For archives, use the pre_get_posts action to modify the main query instead of running a second one. It is faster, and it keeps pagination working.

index.php
PHP
<?php
add_action("pre_get_posts", function (WP_Query $query): void {
    if (!is_admin() && $query->is_main_query() && $query->is_home()) {
        $query->set("posts_per_page", 6);
    }
});

Performance

Every WP_Query runs SQL. Do not run one per item in another loop. Ask only for what you need (fields => "ids", no_found_rows), and consider caching results (see Caching Concepts).

warning
Forgetting wp_reset_postdata()
After a custom loop that uses the_post(), call wp_reset_postdata(). Otherwise functions like the_title() later in the page show the last post of your loop instead of the page’s own post.