In this final lesson we learn how to write your own query of posts and loop through them without interrupting the parent loop in the template. We’ll make a template part for single posts that shows related posts in the same category.

What we will make in this lesson is a related posts query in our single post view, showing a maximum of 3 posts, of post type post, in the same category or categories as the post we’re at, exclude the current post itself, and randomize the posts shown. We’ll also look into strategies for not messing up the nested loop.

Related posts template

Let’s make the related posts bit a template part, which we learned about in part 10. That way we don’t overcomplicate single.php and make our code more reusable. Place the request of this new template part wherever you wish. I’ll place it right before requesting comments template:

single.php
...
	</article>
	<?php 
	get_template_part('related-posts');
	if (comments_open()) {
		comments_template();
	}
	endwhile;
...

Now, let’s make a new empty file in our theme folder and name it related-posts.php. With this file we’re ready to dive into creating a custom post query.

Custom Post Query

If you want to make a custom query of posts, you have some options, but I recommend getting used to the class WP_Query. Bookmark this documentation page as you will refer to it frequently, because there are so many options. I will show you a practical example of setting up a query by referring to the documentation for each specification we want to add.

If you want to learn more about how to query posts, I have a post that goes in depth about just that.

In order to make a custom post query, we will call new WP_Query(), pass an array of arguments to it and store its result in a variable. And then we will use the exact same loop as we have used before in our templates (take a peek at the original loop here to compare). But we need to tell the loop to go through the variable, instead of the global one (which is the single post).

This is how the loop will look like when adding it to a custom query:

$related_posts = new WP_Query([
	// Arguments here
]);
if ($related_posts->have_posts()) {
	while ($related_posts->have_posts()) : $related_posts->the_post();
		// Access to each post here
	endwhile;
	wp_reset_postdata();
}

As you can see, the loop is exactly the same, using have_posts and the_post. The difference is that we call them onto the $related_posts object. If we skip the $related_posts part, WordPress automatically assumes it to be the global query of posts.

You might also notice the function call wp_reset_postdata() right after the loop. This is how we “clean up after ourselves” after a custom query with WP_Query. Remember that the_post() sets up the global post object so that we can use the_title() etc. And as we already are inside a loop (the one in single.php), we need to make sure we clean up and reset the post back to the single post after we’re done. If we do not reset postdata, anything after this will refer to the last related post we went through. This can be a big problem! In our example we have a comments template that follows this. Leaving out the resetting will make the comment template show comments from the last related post in the loop. And not the single post we are actually looking at!

Implementing our custom loop

Alright, let’s start implementing the custom loop in our related-posts.php. I added a wrapper and a title, but as usual you can adjust the HTML as you wish:

related-posts.php
<div class="related-posts">
	<h2><?php _e('Related posts', 'wptutorial'); ?></h2>
	<?php
	$related_posts = new WP_Query([
		
	]);
	if ($related_posts->have_posts()) {
		while ($related_posts->have_posts()) : $related_posts->the_post();

		endwhile;
		wp_reset_postdata();
	}
	?>
</div>

Now we’ll see the power of reusability in template parts. Let’s say that inside this custom loop we want to show the exact same content as we do in our content-loop.php, which we use in our archive templates. All we need to do is to request this template part inside our custom loop, are we’re all set in handling the output of each post!

related-posts.php
...
	while ($related_posts->have_posts()) : $related_posts->the_post();
		get_template_part('content-loop');
	endwhile;
...

Now all that remains is adding the arguments to our post query, making sure we fetch what we want. Let’s go through the arguments one by one.

Building the arguments to our custom post query

We want to fetch no more posts than 3. In the documentation (section “Pagination parameters”) the argument for this is posts_per_page. So we add the array element:

'posts_per_page' => 3

We want to make sure WordPress fetches posts, and not pages or something else. In “Post type parameters” we find:

'post_type' => 'post'

We want to prevent that the single post we’re at, appears in our related posts query, because that makes no sense, right? The documentation tells us in “Post & Page parameters” that we can add the post ID in an array for post__not_in:

'post__not_in' => [get_the_ID()]

We also want to randomize the posts; because as default it will fetch the latest published posts and that can quickly become pretty repetitive as you go through posts. Luckily WordPress has a function for this in how it orders the posts; in “Order and order by parameters” we find:

'orderby' => 'rand'

Finally we want to query posts that are inside the same categories as the post we’re at. To do this, we need to first, before the query arguments, get the categories to the single post we are at. In “Category parameters” we see that we can provide an array of category IDs to category__in. Luckily WordPress has a function for getting category IDs for a post; wp_get_post_categories() which we can use straight out as value to the parameter. Perfect!

Here is the final query and its arguments:

related-posts.php
...
<?php
	$post_cats = wp_get_post_categories(get_the_ID());
	$related_posts = new WP_Query([
		'post_type' => 'post',
		'posts_per_page' => 3,
		'category__in' => $post_cats,
		'post__not_in' => [get_the_ID()],
		'orderby' => 'rand'
	]);
	if ($related_posts->have_posts()) {
...

Refresh and you should see 3 posts listed at the bottom of single view. Refresh multiple times to see that they change because we told WordPress to randomize them. Note: If you have less than 3 posts in the same category, you will get less than 3 posts.

That’s it! Now you know how to query whatever posts you wish. The WP_Query documentation page is extremely helpful in tweaking your query, because it’s really unlimited possibilities here. I encourage you to play around with the parameters, and perhaps try to make a separate template part to use for related posts.