Create WordPress Related Posts Tiles

Learn how to automatically add related posts at the end of your WordPress content. We’ll also create a simple shortcode, so you can insert small related post grids directly into your content, wherever you want. It’ll be responsive, and it’ll work with any post type & taxonomy too 😎

We’re Going to Make This 👇 👇 👇

infoWe won’t cover the animations here. But you can learn how add animations to your Related Posts grid in our WordPress animations tutorial.

How It’s Going to Work

This tutorial uses mostly PHP and CSS. We’re going to make a core function that takes a bunch of parameters, such as “taxonomy” and “number_of_posts”. This core function will create a WP_Query object and use a standard WordPress loop to run the resulting posts through an easy-to-edit PHP/HTML template file. We’ll set up an action-hook-handler so we can automatically add related posts to the end of all single posts.

We’ll keep most of our code separate from the child theme’s functions.php file, so it’ll be easy to reuse the code in other WordPress projects.

Getting Started

importantMake sure you’re using a custom child theme, because you’re going to need to edit your child theme’s functions.php file.

First off, we’ll create some placeholder/scaffold files and connect them up to the child theme. In your custom child theme’s folder, create a new subfolder called wpt-related-posts. In this new sub-folder, create two new/empty text files called related-posts-frontend.css and related-post-template.php. Back in your child theme’s folder, create a text file called wpt-related-posts.php and paste the following code into it.

<?php

/**
 * WP Tutorials Related Posts (WPTRP)
 *
 * https://wp-tutorials.tech/add-functionality/create-wordpress-related-posts-tiles/
 */

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');
}

/**
 * Enqueue the CSS for related posts.
 */
function wptrp_enqueue_assets() {
	global $wptrp_have_assets_been_enqueued;

	if (is_null($wptrp_have_assets_been_enqueued)) {
		$base_url = get_stylesheet_directory_uri() . '/' . pathinfo(__FILE__, PATHINFO_FILENAME) . '/';
		$version = wp_get_theme()->get('Version');

		wp_enqueue_style('wptrp-frontend', $base_url . 'related-posts-frontend.css', null, $version);

		// If you want to enqueue more CSS/JavaScript, do it in here...
		// ...

		$wptrp_have_assets_been_enqueued = true;
	}
}

/**
 * Core logic to generate Related Posts HTML.
 */
function wptrp_get_related_posts(int $post_id = 0, string $taxonomy = 'category', int $number_of_posts = 3, bool $is_title_required = true) {
	// Code will go in here...
	// ...
	// ...

	return '<p>Hello World</p>';
}

/**
 * Automatically add related posts to the end of single post content.
 */
function wptrp_add_related_posts($content) {
	if (!empty($post_id = get_the_ID())) {
		// Add the related-posts HTML to the end of whatever's already
		// in $content.
		$content .= wptrp_get_related_posts($post_id);
	}

	return $content;
}
add_filter('the_content', 'wptrp_add_related_posts');

/**
 * Register a shortcode called wpt_related_posts so we can inject related posts
 * wherever we want to.
 */
function wpt_do_shortcode_related_posts($atts) {
	$html = '';

	if (is_admin()) {
		// Don't do anything.
	} elseif (wp_doing_ajax()) {
		// Don't do anything.
	} else {
		// Process the parameters passed to our shortcode.
		$args = shortcode_atts(
			array(
				'class' => '',
				'post_id' => get_the_ID(),
				'taxonomy' => 'category',
				'count' => 3,
				'show_title' => true,
			),
			$atts
		);

		// Make sure that $post_id is a positive integer.
		if (($post_id = intval($args['post_id'])) <= 0) {
			$html .= sprintf(
				'<p>ERROR: <strong>%s</strong></p>',
				esc_html('No post_id set', 'wp-tutorials')
			);
		} else {
			// Call our main function and return the HTML.
			$html .= wptrp_get_related_posts(
				$post_id,
				sanitize_title($args['taxonomy']),
				intval($args['count']),
				filter_var($args['show_title'], FILTER_VALIDATE_BOOLEAN)
			);
		}
	}

	return $html;
}
add_shortcode('wpt_related_posts', 'wpt_do_shortcode_related_posts');

That’s enough scaffolding to get things set up. Now we need to link to this file from functions.php. Open your child theme’s functions.php file and add the following to it:

/**
 * WP Tutorials Related Posts
 */
require_once 'wpt-related-posts.php';

Save everything, edit some content on your site and add a wpt_related_posts shortcode. Save your content and preview it – you should see Hello World where you put your shortcode.

Alright – we’re making progress.

shortcode for wordpress related posts
Our Related Posts Shortcode

Support Files

The template file (related-post-template.php) is key here, because we’re going to “include” it multiple times as we loop around the related posts. So it’ll be helpful to create a working template now, rather than later on. Open related-post-template.php in the wpt-related-posts subfolder and paste the following into it.

<?php

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');
}

$permalink = get_the_permalink();
$post_title = get_the_title();

?><div class="inner">
    <div class="featured-image">
        <a href="<?php echo esc_url($permalink); ?>">
            <span class="screen-reader-text"><?php echo esc_html($post_title); ?></span>
            <?php the_post_thumbnail('medium');?></a>
    </div>
    <h3><a href="<?php echo esc_url($permalink); ?>"><?php echo $post_title; ?></a></h3>
</div>

This is a straightforward file and you can do what you want in here. Notice you can use standard WordPress loop functions like the_title(), get_the_permalink(), etc.

While we’re at it, open related-posts-frontend.css in the same sub-folder and paste the following into it. Again, you can put what you want in here – this is just to get you started with some basic responsive layout.

/*
 * WPT Related Posts
 * https://wp-tutorials.tech/add-functionality/create-wordpress-related-posts-tiles/
 */

.related-posts {
    padding:  2rem;
}

.related-posts article .inner {
    width:  100%;
    position:  relative;
}

.related-posts article h3 {
    position:  absolute;
    left:  0;
    bottom:  0;
    margin-bottom:  0;
    line-height: 1.5em;
    width:  100%;
    background-color:  #ffffffa0;
}

.related-posts article a,
.related-posts article img {
    display:  block;
}

.related-posts article h3 a {
    padding:  1rem;
}

.related-posts article .featured-image a img {
    width:  100%;
    height:  15em;
    object-fit:  cover;
}

@media(max-width: 919px) {
    .related-posts article:not( :last-child ) {
        padding-bottom:  2rem;
    }
}

@media(min-width: 920px) {
    .related-posts .posts-container {
        display: flex;
        justify-content: space-between;
        gap:  2rem;
    }

    .related-posts article {
        flex-grow: 1;
        flex-basis: 0;
        padding:  0;
    }
}

The Core Function

The logic of the core function will look like this

  • Check / sanitise the input parameters
  • Create a HTML string to hold the title, based on the taxonomy terms associated with the main post
  • Create a $query_args array that will feed into WP_Query
  • If the query has any posts…
    • For each post in the query…
      • Call $query->the_post()
      • Open an <article> element
      • Include the template file
      • Add </article>
    • Call wp_reset_postdata()
  • Return $html

Here’s the big code-lump… Open wpt-related-posts.php and replace the entire contents with this:

<?php

/**
 * WP Tutorials Related Posts (WPTRP)
 *
 * https://wp-tutorials.tech/add-functionality/create-wordpress-related-posts-tiles/
 */

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');
}

/**
 * Enqueue the CSS for related posts.
 */
function wptrp_enqueue_assets() {
	global $wptrp_have_assets_been_enqueued;

	if (is_null($wptrp_have_assets_been_enqueued)) {
		$base_url = get_stylesheet_directory_uri() . '/' . pathinfo(__FILE__, PATHINFO_FILENAME) . '/';
		$version = wp_get_theme()->get('Version');

		wp_enqueue_style('wptrp-frontend', $base_url . 'related-posts-frontend.css', null, $version);

		// If you want to enqueue more CSS/JavaScript, do it in here...
		// ...

		$wptrp_have_assets_been_enqueued = true;
	}
}

/**
 * Core logic to generate Related Posts HTML.
 */
function wptrp_get_related_posts(int $post_id = 0, string $taxonomy = 'category', int $number_of_posts = 3, bool $is_title_required = true) {
	$html = '';
	$post_type = get_post_type($post_id);
	$file_name = 'wpt-related-posts/related-post-template.php';

	// Make sure our assets (CSS) are enqueued.
	wptrp_enqueue_assets();

	// Make a list of term ids and also a term string for our title.
	// You can use get_the_term_list() to make the title, but we're doing it
	// manually here so you can see how you can add extra/custom SEO to your
	// term links, if you want.
	$term_names = '';
	$term_ids = array();
	$terms = get_the_terms($post_id, $taxonomy);
	if (is_array($terms)) {
		foreach ($terms as $term) {
			$term_ids[] = $term->term_id;

			if (!empty($term_names)) {
				$term_names .= ', ';
			}

			$term_names .= sprintf(
				'<a href="%s">%s</a>',
				esc_url(get_term_link($term, $taxonomy)),
				esc_html($term->name)
			);
		}
	}

	// The parameters that will drive our main WP_Query and our loop.
	// 'post__not_in' stops us including the main post in the results.
	$query_args = array(
		'post_type' => $post_type,
		'post_status' => 'publish',
		'posts_per_page' => $number_of_posts,
		'orderby' => 'rand',
		'post__not_in' => array($post_id),
		'tax_query' => array(
			array(
				'taxonomy' => 'category',
				'field' => 'term_id',
				'terms' => $term_ids,
			),
		),
	);

	// DIAGNOSTICS. Uncomment this to print some useful stuff in the HTML.
	// $html .= '<pre>';
	// $html .= print_r($term_ids, true);
	// $html .= print_r($query_args, true);
	// $html .= '</pre>';

	$query = new WP_Query($query_args);
	if ($query->have_posts()) {
		// If you wantto, you can add extra props here...
		// e.g. $props=' data-aos="slide-up"'
		$props = '';

		$html .= sprintf('<div class="related-posts" %s>', $props);

		if ($is_title_required) {
			$html .= sprintf(
				'<h2>%s: %s</h2>',
				esc_html__('Related Posts', 'wp-tutorials'),
				$term_names
			);
		}

		// Start processing the posts that come back from our WP_Query.
		$html .= '<div class="posts-container">';
		while ($query->have_posts()) {
			$query->the_post();

			$html .= sprintf('<article id="post-%d">', get_the_id());

			// Include the template file.
			ob_start();
			include $file_name;
			$html .= ob_get_clean();

			$html .= '</article>'; // #post-999
		}
		$html .= '</div>'; // .posts-container

		$html .= '</div>'; // .related-posts

		wp_reset_postdata();
	}

	return $html;
}

/**
 * Automatically add related posts to the end of single post content.
 */
function wptrp_add_related_posts($content) {
	if (!empty($post_id = get_the_ID())) {
		// Add the related-posts HTML to the end of whatever's already
		// in $content.
		$content .= wptrp_get_related_posts($post_id);
	}

	return $content;
}
add_filter('the_content', 'wptrp_add_related_posts');

/**
 * Register a shortcode called wpt_related_posts so we can inject related posts
 * wherever we want to.
 */
function wpt_do_shortcode_related_posts($atts) {
	$html = '';

	if (is_admin()) {
		// Don't do anything.
	} elseif (wp_doing_ajax()) {
		// Don't do anything.
	} else {
		// Process the parameters passed to our shortcode.
		$args = shortcode_atts(
			array(
				'class' => '',
				'post_id' => get_the_ID(),
				'taxonomy' => 'category',
				'count' => 3,
				'show_title' => true,
			),
			$atts
		);

		// Make sure that $post_id is a positive integer.
		if (($post_id = intval($args['post_id'])) <= 0) {
			$html .= sprintf(
				'<p>ERROR: <strong>%s</strong></p>',
				esc_html('No post_id set', 'wp-tutorials')
			);
		} else {
			// Call our main function and return the HTML.
			$html .= wptrp_get_related_posts(
				$post_id,
				sanitize_title($args['taxonomy']),
				intval($args['count']),
				filter_var($args['show_title'], FILTER_VALIDATE_BOOLEAN)
			);
		}
	}

	return $html;
}
add_shortcode('wpt_related_posts', 'wpt_do_shortcode_related_posts');

Save the file and test it – you should now see related posts at the end of your single posts, and the shortcode should work too.

Win! ⭐ ⭐ ⭐

Have a play with the shortcode options – you can see them in the call to shortcode_atts().

Read through the wptrp_get_related_posts() and locate the key logic elements. Being able to create $query_args and pass it into WP_Query lets you do loads of custom stuff in WordPress.

Uncomment the diagnostics section and reload the content in the frontend – check out the useful dump of info.

commented-out diagnostic code
Diagnostics code commented-out.

Extend the markup in the template file to include more SEO goodies, like schema markup.

That’s it! Have fun sprinkling related posts around your blog/site/shop 😎

Leave a Comment

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