Skip to content

Instantly share code, notes, and snippets.

@abgregs
Last active March 30, 2023 19:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abgregs/2d440edb0c56845b3e3e1a9f4ef26f44 to your computer and use it in GitHub Desktop.
Save abgregs/2d440edb0c56845b3e3e1a9f4ef26f44 to your computer and use it in GitHub Desktop.
A WordPress helper function that takes the ID of the current post and returns the top three related posts ranked by highest number of taxonomy terms in common with the current post.
@lnterr0bang
Copy link

lnterr0bang commented Oct 27, 2021

Hi, this snippet does exactly what I'm looking for a project, but I'm a little confused how to implement it, hoping you can help. When you say:

  • In your template to make use of this function you would do something like...
  • $current_post_id = get_the_id();
  • $related_post_ids = get_related_posts($current_post_id);

I don't quite know what you mean by that. Seems like I need to call that get_related_posts function somehow and then loop it, but I don't quite know the right code for that...

I've done a bunch of stuff with basic queries/loops in templates, but referencing a separate function I haven't done. I've spent hours and hours searching reference docs for how to integrate something like this and there's obviously some basic knowledge being assumed here but I can't figure out what it is so I can patch it together! I'm sure if you could connect the dots for me I could piece it together, I just don't know what I don't know here.

Best I can figure I've put your snippet in functions.php and put this in the template, but this doesn't seem to output anything when it should:

 <?php
 	if ( function_exists( 'get_related_posts' ) ) {

		$current_post_id = get_the_id();
		$related_post_ids = get_related_posts($current_post_id);
			if ( $matching_posts ) {
				foreach ( $matching_post as $post ) {
					setup_postdata( $post ); 
 					echo '<h4>';
					echo the_title();
					echo '</h4>';
					
				}
				wp_reset_postdata();
			}
		}
?>

But that doesn't seem to be doing anything.

Any guidance much appreciated! Thank you!

@abgregs
Copy link
Author

abgregs commented Oct 28, 2021

@lnterr0bang Couple things that may help.

In your snippet, you introduced $matching_posts but never defined it. The $get_related_posts function returns an array of post IDs for the related posts. That's what we want to loop over. So, we just need to check to see if any post IDs were returned.

If related posts were found, our array should have our post IDs. I just updated the gist so it returns an empty array if no related posts were found to make that more straightforward. Inside your loop, you'll be working with a post ID, not a post. So, you'll want to use the appropriate functions to get your post data. See a quick example below.

$current_post_id = get_the_id();
$related_post_ids = get_related_posts($current_post_id);
if ( !empty($related_post_ids) ) {
    foreach ( $related_post_ids as $post_id ) {
        echo '<h2>' . get_the_title($post_id) . '</h2>';
    }
} else {
    // No related posts found
}

Let me know if that gets you pointed in the right direction.

@abgregs
Copy link
Author

abgregs commented Jan 4, 2022

@lnterr0bang Just wanted to check that my note above made sense and see if you got this working. Any luck? Let me know if you have any other questions.

@suf-don
Copy link

suf-don commented Feb 11, 2022

Hi there,

I've tried to implement your code into my Wordpress website but I get the following notice:
Undefined property: stdClass::$count ...

It refers to the following part in the code:

                                    // If no posts have been added yet to $matching_posts then this will be the first.
                                    $new_matching_post = new stdClass();
                                    $new_matching_post->ID = $post_ID;
                                    $new_matching_post->count += $match_count;
                                    array_push($matching_posts, $new_matching_post);

Do you have an idea how to resolve this? Thanks in advance!

@abgregs
Copy link
Author

abgregs commented Feb 11, 2022

Hi @suf-don. That notice won't stop execution so the code should still work, but it is a good catch so thanks for bringing to my attention. Since we are adding the matching post to our array for the very first time in this block of code, we haven't actually set the count previously. Rather than add to the count with +=, we should simply assign the $match_count as the current value. Try replacing with the code below.

// If no posts have been added yet to $matching_posts then this will be the first.
    $new_matching_post = new stdClass();
    $new_matching_post->ID = $post_ID;
    $new_matching_post->count = match_count;
    array_push($matching_posts, $new_matching_post);

What are you seeing displayed on the page? Is it what you expect?

@suf-don
Copy link

suf-don commented Feb 13, 2022

Hi @abgregs, thanks for your quick reply. Your solution works!
Also thank you very much for this function, it was exactly what I needed.

@abgregs
Copy link
Author

abgregs commented Feb 15, 2022

@suf-don Great, happy to hear it works for you!

@CrypticVillain
Copy link

CrypticVillain commented Mar 17, 2023

So this whole thread is very helpful but there are two minor issues.

First the WP_Query on line 39 does not pass the post_per_page argument which by default is 9. This will restrict the total number of post you can compare against to only the first 9 returned by the query.
The fix for this is setting the post_per_page to -1 which in a WP query returns all the posts stored in your database. Add the following line to the arguments after line 39
'posts_per_page' => -1,

The second issue is more of a personal formatting thing.
Obviously you can output posts however you like and then style them with CSS but here's an example of just spitting out the posts as hyperlinks.

$current_post_id = get_the_id();
	$related_post_ids = get_related_posts($current_post_id);
	if ( !empty($related_post_ids) ) {
    	foreach ( $related_post_ids as $post_id ) { ?>
        	<a class='oxy-post' href='<?php the_permalink($post_id); ?>'>
  				<div class='oxy-post-padding'>
    				<div class='oxy-post-image'>
      					<div class='oxy-post-image-fixed-ratio' style='background-image: url(<?php echo get_the_post_thumbnail_url($post_id); ?>);'>
      					</div>            
					</div>
  
    				<div class='oxy-post-overlay'>
      					<h3 class='oxy-post-title'><?php echo get_the_title($post_id); ?></h3>
    				</div>
  				</div>
			</a>
    	<?php }
	} else {
    // No related posts found
	}

Long story short I had to help a friend with their site and while this thread got them 90% of the way there I just wanted to let people know about the potential remaining issues you might face if you just copy paste the above solution.

Here is the code in full:

<?php
/**
 * A WordPress helper function that takes the ID of the current post
 * and returns the top three related posts ranked by highest number of taxonomy
 * terms in common with the current post. Alternately, you can modify lines 26-33
 * to exclude certain taxonomies so that we only check for terms in specific taxonomies 
 * to determine the related posts. To include up to the top X related posts instead of
 * up to three, you can modify lines 148-149.
 *
 * In your template to make use of this function you would do something like...
 *
 * $current_post_id = get_the_id();
 * $related_post_ids = get_related_posts($current_post_id);
 * 
 */

function get_related_posts($current_post_id)
    {   
        // Get the post type we're dealing with based on the current post ID.
        $post_type = get_post_type($current_post_id);

        // Get all taxonomies of the specified post type of the current post.
        $taxonomies = [];
        $taxonomy_objects = get_object_taxonomies( $post_type, 'objects' );
        foreach($taxonomy_objects as $taxonomy) {
            // If you want to only check against certain taxonomies, modify this section as needed
            // to set conditions for which taxonomies should be excluded or included. Below is just an example.
            //if ($taxonomy->name == 'category') {
               //array_push($taxonomies, $taxonomy);
            //}
          
            // By default, we will check against all taxonomies.
            array_push($taxonomies, $taxonomy);
        }

        // Get all the posts of the specified post type,
        // excluding the current post, so that we can compare these
        // against the current post.
        $other_posts_args = array(
            //'suppress_filters' => true,
            'post_type'      => $post_type,
			'posts_per_page'   => -1,
            'post__not_in'   => array($current_post_id),
        );
        $other_posts = new WP_Query( $other_posts_args );

        wp_reset_postdata();

        // We will create an object for each matching post that will include the ID and count of the number of times it matches any taxonomy term with the current post.
        // Later, when we create those, they will get pushed to this $matching_posts array.
        $matching_posts = array();

        // If we have other posts, loop through them and count matches for any taxonomy terms in common.
        if($other_posts->have_posts()) {

            foreach($taxonomies as $taxonomy) {

                // Get the term IDs of terms for the current post
                // (the post presumably displaying as a single post
                // back in our template, for which were finding related posts).
                $current_post_terms = get_the_terms($current_post_id, $taxonomy->name);


                // Only continue if the current post actually has some terms for this taxonomy.
                if($current_post_terms !== false) {

                    foreach($other_posts->posts as $post) {

                        // Get the term IDs of terms for this taxonomy
                        // for the other post we are currently looping over.
                        $other_post_terms = get_the_terms($post->ID, $taxonomy->name);

                        // Check that other post has terms and only continue if there are terms to compare.
                        if($other_post_terms !== false) {

                            $other_post_term_IDs = array();
                            $current_post_term_IDs = array();

                            // Get term IDs from each term in the current post.
                            foreach($current_post_terms as $term) {
                                array_push($current_post_term_IDs, $term->term_id);
                            }

                            // Get term IDs from each term in the other post.
                            foreach($other_post_terms as $term) {
                                array_push($other_post_term_IDs, $term->term_id);
                            }

                            if( !empty($other_post_term_IDs) && !empty($current_post_term_IDs) ) {
                                
                                // Collect the matching term IDs for the terms the posts have in common.
                                $match_count = sizeof(array_intersect($other_post_term_IDs, $current_post_term_IDs));
                                
                                // Get the ID of the other post to use to identify and store this post in our results.
                                $post_ID = $post->ID;

                                if ($match_count > 0) {

                                    // Assume post not added previously.
                                    $post_already_added = false;

                                    // If posts have already been added to our matches then check to see if we already added this post.
                                    if(!empty($matching_posts)) {
        
                                        foreach($matching_posts as $post) {
                                            // If this post was added previously then let's increment the count for our new matching terms.
                                            if (isset($post->ID) && $post->ID == $post_ID) {
                                                $post->count += $match_count;
                                                // Switch this to true for the check we perform below.
                                                $post_already_added = true;
                                            }
                                        }
                                        
                                        // If never found a post with same ID in our $matching_posts list then create a new entry associated with this post and add it.
                                        if ($post_already_added === false) {
                                            $new_matching_post = new stdClass();
                                            $new_matching_post->ID = $post_ID;
                                            $new_matching_post->count = $match_count;
                                            array_push($matching_posts, $new_matching_post);
                                        }
                                    } else {
                                        // If no posts have been added yet to $matching_posts then this will be the first.
                                        $new_matching_post = new stdClass();
                                        $new_matching_post->ID = $post_ID;
                                        $new_matching_post->count = $match_count;
                                        array_push($matching_posts, $new_matching_post);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            
            if(!empty($matching_posts)) {
                // Sort the array in order of highest count for total terms in common
                // (most related to least).
                usort($matching_posts, function($a, $b) {
                    return strcmp($b->count, $a->count);
                });

                // Just take the top 3 most related
                $most_related = array_slice($matching_posts, 0, 3);
                
                // Get the IDs of most related posts.
                $matching_posts = array_map(function($obj) {
                    return $obj->ID;
                }, $most_related);
            }
   
        } 

        return $matching_posts;

    }

// The Loop
	$current_post_id = get_the_id();
	$related_post_ids = get_related_posts($current_post_id);
	if ( !empty($related_post_ids) ) {
    	foreach ( $related_post_ids as $post_id ) { ?>
        	<a class='oxy-post' href='<?php the_permalink($post_id); ?>'>
  				<div class='oxy-post-padding'>
    				<div class='oxy-post-image'>
      					<div class='oxy-post-image-fixed-ratio' style='background-image: url(<?php echo get_the_post_thumbnail_url($post_id); ?>);'>
      					</div>            
					</div>
  
    				<div class='oxy-post-overlay'>
      					<h3 class='oxy-post-title'><?php echo get_the_title($post_id); ?></h3>
    				</div>
  				</div>
			</a>
    	<?php }
	} else {
    // No related posts found
	}

?>

@suf-don
Copy link

suf-don commented Mar 17, 2023 via email

@abgregs
Copy link
Author

abgregs commented Mar 30, 2023

@CrypticVillain Thanks for catching the issue on post limit in the query. I updated the snippet. I'll leave the original intent of the snippet to simply get the related post IDs and let the individual user worry about style and presentation as everyone has their own preference there, but thanks for including your example in the comment and I'm sure others may find it useful. Happy this helped you get most of the way there with your friend's site.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment