How to programmatically get related WordPress posts, easily.
It’s a very common requirement nowadays to want to display related posts (or other post types) underneath your content. It engages readers, provides them with more related material to read, and effectively makes them spend more time on your site, further improving the probability to convert. Related posts also come with added SEO benefits, though minor, as they provide internal links to more of your content (and you probably shouldn’t get obsessed over it).
So, how you should go about adding related posts at your website? Well, it depends. It can be as simple as linking manually to your existing posts, employing semantic analysis algorithms to find related content, or anything in between. However for this tutorial we’ll take a medium route and dust off our PHP skills. We will use the post’s taxonomies and implement it ourselves.
[box type=”info”]TL;DR – This is a lengthy tutorial. If you just want the code snippet needed to get related posts for any post type and any taxonomy, skip to the code.[/box]
The reasoning behind using taxonomies is simple; taxonomies (such as categories and tags) are meant to group things together so if, for example, you have a post in a category named Reviews, it’s only logical that its related posts would come from the same category, and not from News category.
Setting things up
I have created a testing environment for this tutorial where I will use TwentyFourteen as a parent theme, and I will create a child theme where I will be working in. You probably want to follow along creating the child theme for the purposes of this tutorial, or work on your theme files directly. If you don’t know what or why child themes are used for, here is all the info you need on why you should be using them.
First, within your wp-content/themes/ path, create a new folder called related_posts_tut and in there, create an empty style.css file pasting the following contents:
Next, since we want to display the related posts when viewing single posts, we will need to override the single.php file so, make a copy of twentyfourteen/single.php into our folder. While at it, create a an empty functions.php file where we will place various bits of code eventually. You should end up with a file structure similar to Figure 1.
Finally, go to the Dashboard -> Appearance -> Themes and activate the Related Posts theme.
We need some sample content to work with, so let’s create a few posts and assign them on a few categories. Specifically, I have created nine posts and eight categories, detailed below in a Post Name – Category Names format:
|Category 1, Category 1-3
|Category 2, Category 1-3, Category 2-4
|Category 3, Category 1-3, Category 2-4
|Category 4, Category 2-4
|Post No Category
|Post No Category B
With this arrangement of posts and categories we can cover all test cases. Some posts have unique categories, some share categories with many posts, others share a category with only one post, and others have no categories.
Requirements – Assumptions
We don’t have many or strict requirements for this tutorial. All we want is to display a list of related posts below that currently viewed post. Just their titles should be enough for our purposes. We shouldn’t care about styling either. It is assumed that categories are used solely for the purpose of meaningful, semantic grouping of content (as opposed to flagging purposes for example). We define related posts as any two or more posts that share at least one common category.
Let’s see where we will place our related articles first. Fire-up your favorite IDE or editor and open related_posts_tut/single.php. Locate the following lines:
These are the lines that link to the next/previous posts and show the comments template. I want to show my related posts right between those two sections, so my code will be placed between those lines. Let’s create our skeleton first:
At the moment, we just placed some static HTML within the template. I used the <div> element as a wrapper by re-using the post-navigation class, just so that the output will be in par with the rest of the layout. When working in a theme, you’ll have to place it in the appropriate place/wrapper and/or style it appropriately.
Let’s convert this static HTML in to functional WordPress code, shall we?
In order to convert our static HTML into our related posts code, we need first to create a query that will get the related posts and then loop the results and echo the appropriate markup. For the time being, let’s ignore the related part, and just focus on the loop part.
This is pretty much a standard nested loop, which produces the results shown in Figure 2. We create a new query to the database where we get all posts (you don’t want to do this in a real WordPress installation), i.e. where the post_type is post and posts_per_page is -1 (which means unlimited). We then check if the query returned any posts, and if yes, then we go on to print the div wrapper, the heading and the unordered list tags (ul). We wouldn’t want them printed if we weren’t about to show some posts, would we?
Next, we loop through the returned posts and output anything we need by using the standard WordPress template tags. You can show the featured image if you want, the excerpt, and any other post-related information really. We won’t concern ourselves with the actual contents of the loop, other than to confirm that we get correct results from our query. We will concentrate instead on the actual parameters of the WP_Query call, that is the $related_args array.
Please note that from this point onwards, I will stop including the post navigation and comments code that existed prior to our modifications. You should have a good grasp by know of where our focus is.
The query parameters
Let’s start shaping our query parameters by getting some standard stuff out of our way. For example, we only want to get published posts, we never want the currently viewed post to be returned, and we want the results return in random order, so that when there are multiple posts returned, all of them will have a chance of being displayed. With these parameters in mind, the $related_args array should be changed like this:
Figure 3 shows the difference these parameters make, however the post_status effect is not directly visible. It will make a difference if you are logged in and have private posts though. One thing I’d like to point out on the above code is the post__not_in parameter (double underscore after post) is that it requires an array of post IDs, hence the array( get_the_ID() ) code, although we only pass a single ID.
Let’s dive in a bit more now.
It’s time to filter out all the irrelevant posts. That was a lie. It’s actually time to only fetch the posts we need. How? We need to incorporate taxonomy parameters into our query.
[box type=”normal”]While the WP_Query object supports simpler category parameters, we will not be using them since we need added flexibility.[/box]
Our target parameter array is similar to this:
We are adding the tax_query parameter in our query arguments to instruct the WP_Query object to fetch specific terms from a specific taxonomy. Note that the tax_query parameter expects an array of arrays, hence the double array( array( you are seeing. In the tax_query’s sub-parameters, we’re saying that we want to get posts from the taxonomy named category, we want to select categories by their slug (it’s easier to debug), and the specific terms that we want to get results from are array( ‘category-2’, ‘category-1-3’, ‘category-2-4’ ). What we now need is to make this array dynamic, as it is different for every post.
For that, we need the get_the_terms() function. We need to pass it the ID of a post, as well as a taxonomy name, and it will return an array of term objects of that taxonomy that are assigned to the post with that ID.
var_dumping the above line produces the following result for Post 2:
We now need to get the slug field from each object and create an array with them. One way we could go about it is with a foreach loop, just like this:
However this is standard, boring and 4 lines long. It’s such a usual operation that the good WordPress folks have created the wp_list_pluck() function to do just that in only 1 line:
Did we just get the category slugs all in one array? Oh, yes we did! Our taxonomy query parameters are ready, just like that:
Refresh your page, and voila! Figure 4 shows we get Posts 3, 1, and 4 back. Cross-reference that with Table 1, and you’ll see we got the right posts back. Refresh again. We still get the same posts, alas in different order.
At this point, you’ll probably want to check the other posts and confirm that you get correct results. For example, Post 6 is lonely with no related posts, while Post 5 has only one related post, Post 5B. Viewing Post No Category however should rise an error, as Figure 5 shows. This is because get_the_terms() doesn’t find any terms to return, so it returns false, while wp_list_pluck() expects an array.
This can easily be fixed in a lot of ways. My preferred method is to substitute false with an empty array, so that execution will continue, until it ultimately returns a WP_Query object with no results in it. This way, the presentation-related code won’t have to change at all.
Refresh your page. You should be not getting any error, nor any related posts either!
If your only interest is in getting related posts based on categories, you might as well stop reading now. Say thanks in the comments, and go on with your life. If however you feel more adventurous, keep reading…
Refining the code
“What can we do next?” you ask. The answer is: Plenty! How about making the above code reusable, just by calling a function? This way, we can separate the “logic” part of the query from the “presentation” part to make our template files less bloated. Let’s get started, shall we?
First let’s create a new function in our functions.php file (make sure to start the file with a <?php or it won’t work).
We are going to be passing 3 parameters to our function.
- $post_id is the post ID that we want to get the related posts of. We could skip this parameter and make it get it automatically, but that would require you to use the function within a WordPress loop, and would restrict you to getting the related posts of the current current post only. This is not really helpful if you need to display related posts in, say, a widget.
- $related_count is the number of related posts that we want returned. You will usually want to restrict the number of results returned, either because a too big result set is not needed, or because of presentational restrictions.
- $args is an optional array that we can use to pass optional parameters. It can accommodate more parameters as out implementation evolves.
Now, let’s move the PHP code from single.php into the ci_get_related_posts() function. While doing that, you should replace calls to get_the_ID() with $post_id and -1 to $related_count.
Your whole files should now look like this:
This is the last I’m going to be mentioning the single.php file. I will focus instead on improving the ci_get_related_posts() function.
Generalizing the code
We can easily modify our function to take into account tags instead of categories, and products instead of posts, just by changing the hardcoded parameters of the query. This is the reason in the first place, that we didn’t use the Category Parameters of WP_Query. How can we change it though, so that we can use it in any template, for any post type, which may have any number of taxonomies and terms?
We will need to start abstracting things a bit, and construct our query a bit more dynamically. Let’s see first, what our query arguments should look like, if Post 2 had a tag named tag2 and all the categories that we already assigned to it:
The differences between this taxonomy query and our previous one, are just two. First, we add another array into the tax_query array, only this time the taxonomy is post_tag and we pass the slugs of the tags in the terms array. What we really need to make this generic, is to create one such array for each taxonomy that the current post supports and has terms for. Second, we must add the OR relation parameter, so that the query will match posts from either the first taxonomy, or the second, et cetera for each taxonomy. We could change OR to AND, however this will really narrow down the results returned, as you should have posts that have both at least one common category and at least one common tag, et cetera for each taxonomy.
[box type=”warning”]According to the documentation, The relationship operator ‘relation’ must only be set when there are at least two inner taxonomy arrays.[/box]
So, let’s try and make the taxonomy query dynamic. First, we are gonna start with an empty tax_query and remove the terms related code that we have:
Next, we need to get the taxonomies related to our post. For this, we are going to use the get_object_taxonomies() function, which requires a post object (or a post type name), and we will instruct it to return only the names of the taxonomies (with the alternative being taxonomy objects). We are also going to use the get_post() function first to get our post object so that we can then pass it along.
var_dumping the $taxonomies variable for Post 2 gives us this list:
Now, since we know the names of the post’s taxonomies, we can use them in a loop to get_the_terms().:
And once we have them, we can add an inner array to tax_query:
However, there is always the chance that $terms is empty, since a post might have a taxonomy registered, but no terms assigned to it, just like our Post No Category post. So, let’s handle this case by skipping execution to the next taxonomy, using the continue statement.
There are only a couple of things left to do now. We need to add the relation argument, if and only if the tax_query‘s arrays are two or more. Since tax_query is an array itself, we can count how many elements (sub-arrays) we added, simply using count():
Let’s var_dump( $related_args[‘tax_query’] ); to see what we get for Post 2:
That’s essentially the same as the array at the beginning of this section, where we stated what our array should look like.
One final thing to really make our code generic, is substitute the hard-coded post_type with the appropriate one, depending on the $post_id passed. So, replace:
This is it! You are done! Your ci_get_related_posts() function should now look like this:
Remember that lonely $args array that the function accepts but we never used? Let’s show some pity on it and make it useful:
We just added a few lines of code that can make our little function even more useful.
$args now can optionally take two more parameters, orderby and return. We give them default values via the wp_parse_args() function, so we can omit them unless we need them. We pass orderby as a parameter to the orderby query parameter, so we can pass values such as rand, date, author, etc to order the results differently.
Finally, we can instruct the function to return the $related_args array instead of returning a new WP_Query object, by setting return to array instead of query, in case you want to further manipulate it before performing the actual query.
With these parameters in place, you can now call the function in all sorts of different ways:
This is the complete code for this tutorial. You need to paste this function into your (child preferably) theme’s functions.php file:
Inside your template file, most probably single.php, you will need to add code similar to this:
Don’t forget that you will probably need to style your related posts with CSS appropriately.
As you can see, creating something seemingly difficult is actually quite easy, once you know your way around WordPress and its functions. Even if you don’t, taking things simply and then refining them, having on hand the WordPress Function Reference and a good search engine, will take you a long way.
So, what do you think? Has this tutorial taught you anything? Has this approach helped you anywhere? Let me know in the comments below.