The Loop is the PHP code that goes through the posts WordPress found for the current request and prints each one.
<?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
| Prints | Returns |
|---|---|
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.
<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.
<?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
| Argument | Meaning |
|---|---|
post_type | post, page, or a custom post type |
posts_per_page | How many posts (-1 for all, use with care) |
post_status | publish, draft … |
orderby, order | Sorting |
category_name, tag, tax_query | Filter by taxonomy |
meta_key, meta_query | Filter by custom fields |
s | Search term |
paged | Page number for pagination |
post__in, post__not_in | Include or exclude IDs |
no_found_rows | Set true when you do not need pagination (faster) |
You can test building arguments with plain PHP, because they are only an array:
<?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));
{"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.
<?php
$posts = get_posts(["numberposts" => 3, "post_type" => "post"]);
foreach ($posts as $post) {
echo esc_html($post->post_title);
}
Pagination
<?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.
<?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).
Forgetting wp_reset_postdata()
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.