How to programmatically get related WordPress posts, easily.

rp

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.

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.

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.

This tutorial assumes some prior knowledge, specifically being familiar with the basics of The Loop and the WP_Query object.

Setting things up

Child theme

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:

/*
 Theme Name:   Related Posts
 Theme URI:    https://www.cssigniter.com/ignite/programmatically-get-related-wordpress-posts-easily/
 Description:  Related Posts Tutorial (child theme of Twenty Fourteen)
 Author:       Anastis Sourgoutsidis
 Author URI:   https://www.cssigniter.com
 Template:     twentyfourteen
 Version:      1.0.0
*/

@import url("../twentyfourteen/style.css");
Screenshot of the Related Posts child theme file structure

Figure 1 – Child theme file structure

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.

Sample content

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:

Table 1 – Posts
Post name Category name
Post 1 Category 1, Category 1-3
Post 2 Category 2, Category 1-3, Category 2-4
Post 3 Category 3, Category 1-3, Category 2-4
Post 4 Category 4, Category 2-4
Post 5 Category 5
Post 5B Category 5
Post 6 Category 6
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.

Implementation

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:

// Previous/next post navigation.
twentyfourteen_post_nav();

// If comments are open or we have at least one comment, load up the comment template.
if ( comments_open() || get_comments_number() ) {
	comments_template();
}

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:

// Previous/next post navigation.
twentyfourteen_post_nav();

?>
	<div class="post-navigation">
		<h3>Related posts</h3>
		<ul>
			<li>Test</li>
		</ul>
	</div>
<?php

// If comments are open or we have at least one comment, load up the comment template.
if ( comments_open() || get_comments_number() ) {
	comments_template();
}

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?

The Loop

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.

// Previous/next post navigation.
twentyfourteen_post_nav();

$related_args = array(
	'post_type' => 'post',
	'posts_per_page' => -1,
);
$related = new WP_Query( $related_args );

if( $related->have_posts() ):
?>
	<div class="post-navigation">
		<h3>Related posts</h3>
		<ul>
			<?php while( $related->have_posts() ): $related->the_post(); ?>
				<li><?php the_title(); ?></li>
			<?php endwhile; ?>
		</ul>
	</div>
<?php
endif;
wp_reset_postdata();

// If comments are open or we have at least one comment, load up the comment template.
if ( comments_open() || get_comments_number() ) {
	comments_template();
}

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.

Screenshot of all posts query

Figure 2 – Result of all querying all posts.

 

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:

$related_args = array(
	'post_type'      => 'post',
	'posts_per_page' => -1,
	'post_status'    => 'publish',
	'post__not_in'   => array( get_the_ID() ),
	'orderby'        => 'rand',
);

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.

Screenshot of random posts query

Figure 3 – Result of random posts query.

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.

While the WP_Query object supports simpler category parameters, we will not be using them since we need added flexibility.

Our target parameter array is similar to this:

$related_args = array(
	'post_type'      => 'post',
	'posts_per_page' => -1,
	'post_status'    => 'publish',
	'post__not_in'   => array( get_the_ID() ),
	'orderby'        => 'rand',
	'tax_query'      => array(
		array(
			'taxonomy' => 'category',
			'fields'   => 'slug',
			'terms'    => array( 'category-2', 'category-1-3', 'category-2-4' )
		)
	)
);

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.

$terms = get_the_terms( get_the_ID(), 'category' );

var_dumping the above line produces the following result for Post 2:

array (size=3)
  468 =>
    object(stdClass)[373]
      public 'term_id' => &int 468
      public 'name' => &string 'Category 1-3' (length=12)
      public 'slug' => &string 'category-1-3' (length=12)
      public 'term_group' => int 0
      public 'term_taxonomy_id' => int 497
      public 'taxonomy' => string 'category' (length=8)
      public 'description' => &string '' (length=0)
      public 'parent' => &int 0
      public 'count' => &int 3
      public 'object_id' => int 6125
      public 'filter' => string 'raw' (length=3)
      public 'cat_ID' => &int 468
      public 'category_count' => &int 3
      public 'category_description' => &string '' (length=0)
      public 'cat_name' => &string 'Category 1-3' (length=12)
      public 'category_nicename' => &string 'category-1-3' (length=12)
      public 'category_parent' => &int 0
  465 =>
    object(stdClass)[376]
      public 'term_id' => &int 465
      public 'name' => &string 'Category 2' (length=10)
      public 'slug' => &string 'category-2' (length=10)
      ...

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:

$term_list = array();
foreach( $terms as $term ) {
	$term_list[] = $term->slug;
}

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:

$term_list = wp_list_pluck( $terms, 'slug' );

Did we just get the category slugs all in one array? Oh, yes we did! Our taxonomy query parameters are ready, just like that:

$terms        = get_the_terms( get_the_ID(), 'category' );
$term_list    = wp_list_pluck( $terms, 'slug' );
$related_args = array(
	'post_type'      => 'post',
	'posts_per_page' => -1,
	'post_status'    => 'publish',
	'post__not_in'   => array( get_the_ID() ),
	'orderby'        => 'rand',
	'tax_query'      => array(
		array(
			'taxonomy' => 'category',
			'field'    => 'slug',
			'terms'    => $term_list
		)
	)
);

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.

Screenshot of a post and its related posts (based on matching categories)

Figure 4 – Post 2 and its related posts.

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.

Screenshot of wp_list_pluck() error

Figure 5 – wp_list_pluck() throwing a warning.

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.

$terms        = get_the_terms( $post_id, 'category' );
if( empty( $terms ) ) $terms = array();
$term_list    = wp_list_pluck( $terms, 'slug' );

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

function ci_get_related_posts( $post_id, $related_count, $args = array() ) {
	// Will add code here.
}

We are going to be passing 3 parameters to our function.

  1. $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.
  2. $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.
  3. $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:
functions.php

<?php
	function ci_get_related_posts( $post_id, $related_count, $args = array() ) {
		$terms        = get_the_terms( $post_id, 'category' );
		if ( empty( $terms ) ) $terms = array();
		$term_list    = wp_list_pluck( $terms, 'slug' );
		$related_args = array(
			'post_type'      => 'post',
			'posts_per_page' => $related_count,
			'post_status'    => 'publish',
			'post__not_in'   => array( $post_id ),
			'orderby'        => 'rand',
			'tax_query'      => array(
				array(
					'taxonomy' => 'category',
					'field'    => 'slug',
					'terms'    => $term_list
				)
			)
		);

		return new WP_Query( $related_args );
	}

single.php

<?php
/**
 * The Template for displaying all single posts
 *
 * @package WordPress
 * @subpackage Twenty_Fourteen
 * @since Twenty Fourteen 1.0
 */

get_header(); ?>

	<div id="primary" class="content-area">
		<div id="content" class="site-content" role="main">
			<?php
				// Start the Loop.
				while ( have_posts() ) : the_post();

					/*
					 * Include the post format-specific template for the content. If you want to
					 * use this in a child theme, then include a file called called content-___.php
					 * (where ___ is the post format) and that will be used instead.
					 */
					get_template_part( 'content', get_post_format() );

					// Previous/next post navigation.
					twentyfourteen_post_nav();

					$related = ci_get_related_posts( get_the_ID(), -1 );

					if( $related->have_posts() ):
					?>
						<div class="post-navigation">
							<h3>Related posts</h3>
							<ul>
								<?php while( $related->have_posts() ): $related->the_post(); ?>
									<li><?php the_title(); ?></li>
								<?php endwhile; ?>
							</ul>
						</div>
					<?php
					endif;
					wp_reset_postdata();

					// If comments are open or we have at least one comment, load up the comment template.
					if ( comments_open() || get_comments_number() ) {
						comments_template();
					}
				endwhile;
			?>
		</div><!-- #content -->
	</div><!-- #primary -->

<?php
get_sidebar( 'content' );
get_sidebar();
get_footer();

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:

$related_args = array(
	'post_type'      => 'post',
	'posts_per_page' => -1,
	'post_status'    => 'publish',
	'post__not_in'   => array( get_the_ID() ),
	'orderby'        => 'rand',
	'tax_query'      => array(
		'relation' => 'OR',
		array(
			'taxonomy' => 'category',
			'fields'   => 'slug',
			'terms'    => array( 'category-2', 'category-1-3', 'category-2-4' )
		),
		array(
			'taxonomy' => 'post_tag',
			'fields'   => 'slug',
			'terms'    => array( 'tag2' )
		),
	)
);

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.

According to the documentation, The relationship operator ‘relation’ must only be set when there are at least two inner taxonomy arrays.

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:

function ci_get_related_posts( $post_id, $related_count, $args = array() ) {
	$related_args = array(
		'post_type'      => 'post',
		'posts_per_page' => $related_count,
		'post_status'    => 'publish',
		'post__not_in'   => array( $post_id ),
		'orderby'        => 'rand',
		'tax_query'      => array()
	);

	return new WP_Query( $related_args );
}

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.

$post       = get_post( $post_id );
$taxonomies = get_object_taxonomies( $post, 'names' );

var_dumping the $taxonomies variable for Post 2 gives us this list:

array (size=3)
  0 => string 'category' (length=8)
  1 => string 'post_tag' (length=8)
  2 => string 'post_format' (length=11)

Now, since we know the names of the post’s taxonomies, we can use them in a loop to get_the_terms().:

foreach( $taxonomies as $taxonomy ) {
	$terms = get_the_terms( $post_id, $taxonomy );
}

And once we have them, we can add an inner array to tax_query:

foreach( $taxonomies as $taxonomy ) {
	$terms = get_the_terms( $post_id, $taxonomy );
	$term_list = wp_list_pluck( $terms, 'slug' );
	$related_args['tax_query'][] = array(
		'taxonomy' => $taxonomy,
		'field'    => 'slug',
		'terms'    => $term_list
	);
}

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.

foreach( $taxonomies as $taxonomy ) {
	$terms = get_the_terms( $post_id, $taxonomy );
	if ( empty( $terms ) ) continue;
	$term_list = wp_list_pluck( $terms, 'slug' );
	$related_args['tax_query'][] = array(
		'taxonomy' => $taxonomy,
		'field'    => 'slug',
		'terms'    => $term_list
	);
}

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():

if( count( $related_args['tax_query'] ) > 1 ) {
	$related_args['tax_query']['relation'] = 'OR';
}

Let’s var_dump( $related_args[‘tax_query’] ); to see what we get for Post 2:

array (size=3)
  0 =>
    array (size=3)
      'taxonomy' => string 'category' (length=8)
      'field' => string 'slug' (length=4)
      'terms' =>
        array (size=3)
          468 => string 'category-1-3' (length=12)
          465 => string 'category-2' (length=10)
          469 => string 'category-2-4' (length=12)
  1 =>
    array (size=3)
      'taxonomy' => string 'post_tag' (length=8)
      'field' => string 'slug' (length=4)
      'terms' =>
        array (size=1)
          472 => string 'tag2' (length=4)
  'relation' => string 'OR' (length=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:

'post_type' => 'post',

with:

'post_type' => get_post_type( $post_id ),

This is it! You are done! Your ci_get_related_posts() function should now look like this:

function ci_get_related_posts( $post_id, $related_count, $args = array() ) {
	$related_args = array(
		'post_type'      => get_post_type( $post_id ),
		'posts_per_page' => $related_count,
		'post_status'    => 'publish',
		'post__not_in'   => array( $post_id ),
		'orderby'        => 'rand',
		'tax_query'      => array()
	);

	$post       = get_post( $post_id );
	$taxonomies = get_object_taxonomies( $post, 'names' );

	foreach( $taxonomies as $taxonomy ) {
		$terms = get_the_terms( $post_id, $taxonomy );
		if ( empty( $terms ) ) continue;
		$term_list = wp_list_pluck( $terms, 'slug' );
		$related_args['tax_query'][] = array(
			'taxonomy' => $taxonomy,
			'field'    => 'slug',
			'terms'    => $term_list
		);
	}

	if( count( $related_args['tax_query'] ) > 1 ) {
		$related_args['tax_query']['relation'] = 'OR';
	}

	return new WP_Query( $related_args );
}

Polishing

Remember that lonely $args array that the function accepts but we never used? Let’s show some pity on it and make it useful:

function ci_get_related_posts( $post_id, $related_count, $args = array() ) {
	$args = wp_parse_args( (array) $args, array(
		'orderby' => 'rand',
		'return'  => 'query', // Valid values are: 'query' (WP_Query object), 'array' (the arguments array)
	) );

	$related_args = array(
		'post_type'      => get_post_type( $post_id ),
		'posts_per_page' => $related_count,
		'post_status'    => 'publish',
		'post__not_in'   => array( $post_id ),
		'orderby'        => $args['orderby'],
		'tax_query'      => array()
	);

	$post       = get_post( $post_id );
	$taxonomies = get_object_taxonomies( $post, 'names' );

	foreach( $taxonomies as $taxonomy ) {
		$terms = get_the_terms( $post_id, $taxonomy );
		if ( empty( $terms ) ) continue;
		$term_list = wp_list_pluck( $terms, 'slug' );
		$related_args['tax_query'][] = array(
			'taxonomy' => $taxonomy,
			'field'    => 'slug',
			'terms'    => $term_list
		);
	}

	if( count( $related_args['tax_query'] ) > 1 ) {
		$related_args['tax_query']['relation'] = 'OR';
	}

	if( $args['return'] == 'query' ) {
		return new WP_Query( $related_args );
	} else {
		return $related_args;
	}
}

We just added a few lines of code that can make our little function even more useful.

$args = wp_parse_args( (array) $args, array(
	'orderby' => 'rand',
	'return'  => 'query', // Valid values are: 'query' (WP_Query object), 'array' (the arguments array)
) );
...
	'orderby'        => $args['orderby'],
...
if( $args['return'] == 'query' ) {
	return new WP_Query( $related_args );
} else {
	return $related_args;
}

$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:

// Get unlimited related posts for the current post, ordered by date.
$related_posts = ci_get_related_posts( get_the_ID(), -1, array(
	'orderby' => 'date'
) );

...

// Get the query array for 2 related posts for the post with ID = 5, random ordered.
$related_args = ci_get_related_posts( 5, 2, array(
	'return' => 'array'
) );
$related_posts = new WP_Query( $related_args );

...

// Get 5 related posts for the post with ID = $some_post_id, random ordered.
$related_posts = ci_get_related_posts( $some_post_id, 5 );

...

// Get the query array for unlimited related posts for the current post, ordered by author.
$related_args = ci_get_related_posts( get_the_ID(), -1, array(
	'return' => 'array',
	'orderby' => 'author'
) );

The Code

This is the complete code for this tutorial. You need to paste this function into your (child preferably) theme’s functions.php file:

function ci_get_related_posts( $post_id, $related_count, $args = array() ) {
	$args = wp_parse_args( (array) $args, array(
		'orderby' => 'rand',
		'return'  => 'query', // Valid values are: 'query' (WP_Query object), 'array' (the arguments array)
	) );

	$related_args = array(
		'post_type'      => get_post_type( $post_id ),
		'posts_per_page' => $related_count,
		'post_status'    => 'publish',
		'post__not_in'   => array( $post_id ),
		'orderby'        => $args['orderby'],
		'tax_query'      => array()
	);

	$post       = get_post( $post_id );
	$taxonomies = get_object_taxonomies( $post, 'names' );

	foreach( $taxonomies as $taxonomy ) {
		$terms = get_the_terms( $post_id, $taxonomy );
		if ( empty( $terms ) ) continue;
		$term_list = wp_list_pluck( $terms, 'slug' );
		$related_args['tax_query'][] = array(
			'taxonomy' => $taxonomy,
			'field'    => 'slug',
			'terms'    => $term_list
		);
	}

	if( count( $related_args['tax_query'] ) > 1 ) {
		$related_args['tax_query']['relation'] = 'OR';
	}

	if( $args['return'] == 'query' ) {
		return new WP_Query( $related_args );
	} else {
		return $related_args;
	}
}

Inside your template file, most probably single.php, you will need to add code similar to this:

$related = ci_get_related_posts( get_the_ID(), 3 );

if( $related->have_posts() ):
	?>
	<div class="related-posts">
		<h3>Related posts</h3>
		<ul>
			<?php while( $related->have_posts() ): $related->the_post(); ?>
				<li>
					<h4><?php the_title(); ?></h4>
					<?php the_excerpt(); ?>
				</li>
			<?php endwhile; ?>
		</ul>
	</div>
	<?php
endif;
wp_reset_postdata();

Don’t forget that you will probably need to style your related posts with CSS appropriately.

Conclusion

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.

13 comments

  1. Nathan says:

    Great article. It’s over my head but I got here searching for a way to add date parameter code to related posts. That’s one thing not covered in this article. For some sites it doesn’t make sense to show posts from a few years ago, and it becomes a problem when using random order. Could you offer any advice on how to add date parameters?

    Like how would it be added to an example like this:

    $query_args = array(
    ‘limit’ => $atts[‘limit’],
    ‘post_type’ => $post_type,
    ‘taxonomies’ => $taxonomies_string,
    ‘specific_terms’ => $specific_terms,
    ‘order’ => ‘DESC’,
    ‘orderby’ => ‘date’,
    ‘exclude’ => array( $post->ID )
    );

    I found the date parameter codes from the article below but I don’t know how to implement it properly.

    http://codex.wordpress.org/Class_Reference/WP_Query#Date_Parameters

    Thanks!

  2. Amanda says:

    This is awesome! No other code I tried and fumbled with worked. But a question for you, how could I make this so it’s a link? This currently doesn’t link to the related post as is, it only displays it.

    1. Anastis Sourgoutsidis says:

      You can change, for example this:

      <h4><?php the_title(); ?></h4>

      to this:

      <h4><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h4>
      1. Amanda says:

        Thank you so much!

  3. Gabrielle says:

    Hi. Thank you for the great article! Definitely the most complete one I could find. I was just wondering if there’s a way to filter the related posts so it won’t show posts older than X months, or a set year, or anything like it. My blog is a few years old and it’s showing posts from years ago that aren’t really relevant anymore. Thank you!

    1. Anastis Sourgoutsidis says:

      Similarly to the tax_query explained above, there’s also a date_query you can use to place data-based limits.
      For example, you could add to your query something like this:

      $related_args['date_query'] = array(
          'after' => array( 'year' => 2014 )
      );
      
  4. Rebel says:

    Hi, thanks for great article.

    Can I ask how to related by title and category only?

    Thanks

    1. Anastis Sourgoutsidis says:

      I’m not sure I understand what you mean “by title”. Could you please elaborate?

      1. Rebel says:

        I tried something like this

        $related_args = array(
        ‘post_type’ => get_post_type( $post_id ),
        ‘posts_per_page’ => $related_count,
        ‘post_status’ => ‘publish’,
        ‘s’ => get_the_title( $post_id )
        );

        If one post name or title is “Love foo” and another one is “Foo with love” and they are in the same taxonomy category, then related each other.

        1. Alfan says:

          Hi, it seems a good solution by adding ‘s’

          Well, here mya case, in order to display the most related then I also use the title of each post

          Besides that, I also use custom filed namely mytitle in every single post…

          How can I combine those post title + my own custom field?

          Anyway, thanks a lot for the complete tutorial!

          1. Anastis Sourgoutsidis says:

            As above, default WordPress search won’t cut it. Perhaps you can use something like https://wordpress.org/plugins/relevanssi/ that allows you to search on custom fields.

            Please note that I don’t have any experience with this plugin, so you’re pretty much on your own :D

        2. Anastis Sourgoutsidis says:

          Actually, the ‘s’ (search) parameter is not a very good solution, at least while using the default WordPress functionality. When searching “Love foo”, it will successfully match “Foo with love” as all words (love+foo) match. However, the reverse will not work. When searching “Foo with love” will NOT return “Love foo” as it’s missing the term “with”.
          For your requirements, you’re probably better off using a plugin that employs some kind of language understanding, such as https://wordpress.org/plugins/yet-another-related-posts-plugin/ or similar. Or perhaps something like this: https://wordpress.org/plugins/relevanssi/

          1. Si Retu says:

            Well, I use both of them, and it is no wonder if those plugin can be categorized as high resource.

            Actually Relevanssi can be used for related post too but the creator said that it is not recommended…

            I’m trying to use ‘s’ for related post but it doesn’t work if I install Relevanssi.. After I deactive it then the query runs well..

            Btw thanks for the tutorial..

Leave a Reply

Your email address will not be published. Required fields are marked *