Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: related posts

  • Rolling Your Own Related Posts

    Rolling Your Own Related Posts

    To start out at the top, I did not write a whole ‘related posts’ plugin. As with all things, I started by asking myself “What’s the problem I’m trying to solve?”

    The answer is “I have a custom post type that needs to relate to other posts in the type, but based on my specific criteria which is currently organized into custom taxonomies and post meta.” And from the outset, that certainly sounds like a massive custom job. it was one I was dreading until I remembered that a developer I respected and trusted had once complained to me about the problems with all those other auto-related-posts plugins.

    1. They’re heavy and use a lot of ram
    2. They don’t let you customize ‘weight’ of relations
    3. They’re not extendable

    So I did the next logical thing and I looked up their plugins.

    The Plugin

    The crux of why I chose this plugin was simply that it’s extendable, but also that it started out with what I had:

    Posts with the most terms in common will display at the top!

    Perfect!

    Design The Basics

    Before you jump into coding, you need to know what you’re doing. I chose to isolate what I needed first. I made a list of everything I thought was relative:

    • Taxonomies: Tropes, Genres, Intersectionality, Tags, Stars
    • Post Meta: Worth It, Loved, Calculated Score

    Yes, it’s that site again.

    I read the plugin documentation and verified that for most of that I just needed to list the taxonomies in the shortcode like this:

    [related_posts_by_tax fields="ids" order="RAND" title="" format="thumbnails" image_size="postloop-img" link_caption="true" posts_per_page="6" columns="0" post_class="similar-shows" taxonomies="lez_tropes,lez_genres,lez_stars,lez_intersections,lez_showtagged"]
    

    Initially I didn’t list the stars because the way the code works, it would say “If you have a Gold Star, show other Gold Stars.” And that wasn’t what I wanted to see. I wanted “If you have ANY star, show other shows with a star.” That said, once we got over 12 shows in each ‘star’ category, this became much easier to match and I could add it in.

    The rest of the code, those checks for meta, needed actual code written.

    Meta Checks

    There’s a helpful filter, related_posts_by_taxonomy_posts_meta_query, that lets you filter the meta queries used by the post. Leveraging that, we can make our checks:

    1. Match the ‘worth it’ value of a show
    2. If the show is loved, list other loved show
    3. If the show isn’t loved, use the score to find show with the same relative value

    Both Worth It and Loved are post meta values. Mine happen to be added by CMB2, but the logic remains the same regardless how you add it. Worth It has four possible values (Yes, No, Maybe, TBD), and the check is either the value or false. Loved is a checkbox, a boolean exists or not, which means it’s a true/falsy. The score is a number that’s generated every time the show is saved, and it’s crazy complicated and another story.

    The code I use looks like this:

    add_filter( 'related_posts_by_taxonomy_posts_meta_query', 'MYSITE_RPBT_meta_query', 10, 4 );
    function MYSITE_RPBT_meta_query( $meta_query, $post_id, $taxonomies, $args ) {
    	$worthit = ( get_post_meta( $post_id, 'lezshows_worthit_rating', true ) ) ? get_post_meta( $post_id, 'lezshows_worthit_rating', true ) : false;
    	$loved   = ( get_post_meta( $post_id, 'lezshows_worthit_show_we_love', true ) ) ? true : false;
    	$score   = ( get_post_meta( $post_id, 'lezshows_the_score', true ) ) ? get_post_meta( $post_id, 'lezshows_the_score', true ) : 10;
    
    	// We should match up the worth-it value as well as the score.
    	// After all, some low scores have a thumbs up.
    	if ( false !== $worthit ) {
    		$meta_query[] = array(
    			'key'     => 'lezshows_worthit_rating',
    			'compare' => $worthit,
    		);
    	}
    
    	// If the show is loved, we want to include it here.
    	if ( $loved ) {
    		$meta_query[] = array(
    			'key'     => 'lezshows_worthit_show_we_love',
    			'compare' => 'EXISTS',
    		);
    	}
    
    	// If they're NOT loved, we use the scores for a value.
    	if ( ! $loved ) {
    		// Score: If the score is similar +/- 10
    		if ( $score >= 90 ) {
    			$score_range = array( 80, 100 );
    		} elseif ( $score <= 10 ) {
    			$score_range = array( 10, 30 );
    		} else {
    			$score_range = array( ( $score - 10 ), ( $score + 10 ) );
    		}
    		$meta_query[] = array(
    			'key'     => 'lezshows_the_score',
    			'value'   => $score_range,
    			'type'    => 'numeric',
    			'compare' => 'BETWEEN',
    		);
    	}
    
    	return $meta_query;
    }
    

    More Similar

    But there’s one more thing we wanted to include. When I built this out, Tracy said “There should be a way for us to pick the shows we think are similar!”

    She’s right! I built in a CMB2 repeatable field where you can pick shows from a dropdown and that saves the show post IDs as an array. That was the easy part, since we were already doing that in another place.

    Once that list exists, we grab the handpicked list, break it out into a simple array, check if the post is published and not already on the list, and combine it all:

    add_filter( 'related_posts_by_taxonomy', array( $this, 'alter_results' ), 10, 4 );
    function alter_results( $results, $post_id, $taxonomies, $args ) {
    	$add_results = array();
    
    	if ( ! empty( $results ) && empty( $args['fields'] ) ) {
    		$results = wp_list_pluck( $results, 'ID' );
    	}
    
    	$handpicked  = ( get_post_meta( $post_id, 'lezshows_similar_shows', true ) ) ? wp_parse_id_list( get_post_meta( $post_id, 'lezshows_similar_shows', true ) ) : array();
    	$reciprocity = self::reciprocity( $post_id );
    	$combo_list  = array_merge( $handpicked, $reciprocity );
    
    	if ( ! empty( $combo_list ) ) {
    		foreach ( $combo_list as $a_show ) {
    			//phpcs:ignore WordPress.PHP.StrictInArray
    			if ( 'published' == get_post_status( $a_show ) && ! in_array( $a_show, $results ) && ! in_array( $a_show, $add_results ) ) {
    				$add_results[] = $a_show;
    			}
    		}
    	}
    
    	$results = $add_results + $results;
    
    	return $results;
    }
    

    But … you may notice $reciprocity and wonder what that is.

    Well, in a perfect world if you added The Good Fight as a show similar to The Good Wife, you’d also go back and add The Good Wife to The Good Fight. The reality is humans are lazy. There were two ways to solve this reciprocity of likes issues.

    1. When a show is added as similar to a show, the code auto-adds it to the other show
    2. When the results are generated, the code checks if any other show likes the current show and adds it

    Since we’re already having saving speed issues (there’s a lot of back processing going on with the scores) and I’ve integrated caching, it was easier to pick option 2.

    function reciprocity( $post_id ) {
    	if ( ! isset( $post_id ) || 'post_type_shows' !== get_post_type( $post_id ) ) {
    		return;
    	}
    
    	$reciprocity      = array();
    	$reciprocity_loop = new WP_Query(
    		array(
    			'post_type'              => 'post_type_shows',
    			'post_status'            => array( 'publish' ),
    			'orderby'                => 'title',
    			'order'                  => 'ASC',
    			'posts_per_page'         => '100',
    			'no_found_rows'          => true,
    			'update_post_term_cache' => true,
    			'meta_query'             => array(
    				array(
    					'key'     => 'lezshows_similar_shows',
    					'value'   => $post_id,
    					'compare' => 'LIKE',
    				),
    			),
    		)
    	);
    
    	if ( $reciprocity_loop->have_posts() ) {
    		while ( $reciprocity_loop->have_posts() ) {
    			$reciprocity_loop->the_post();
    			$this_show_id = get_the_ID();
    			$shows_array  = get_post_meta( $this_show_id, 'lezshows_similar_shows', true );
    
    			if ( 'publish' === get_post_status( $this_show_id ) && isset( $shows_array ) && ! empty( $shows_array ) ) {
    				foreach ( $shows_array as $related_show ) {
    					if ( $related_show == $post_id ) {
    						$reciprocity[] = $this_show_id;
    					}
    				}
    			}
    		}
    		wp_reset_query();
    		$reciprocity = wp_parse_id_list( $reciprocity );
    	}
    
    	return $reciprocity;
    }
    

    There’s a little looseness with the checks, and because there are some cases were shows show up wrong because of the ids (ex: show 311 and 3112 would both be positive for a check on 311), we have to double up on the checks to make sure that the show is really the same.

    What’s Next?

    There are still some places I could adjust this. Like if I use more filters I can make the show stars worth ‘more’ than the genres and so on. And right now, due to the way most Anime are based on Manga (and thus get flagged as “Literary Inspired”), anything based on Sherlock Holmes ends up with a lot of recommended Anime.

    Still, this gives me a way more flexible way to list what’s similar.

  • Types of Related Posts

    Types of Related Posts

    At it’s heart, related posts are the drive to help people find more content on your site. They serve no other purpose than keeping people on your site by piquing their interest in your words.

    But what if I told you there were multiple types of related posts for WordPress?

    Categories and Tags

    The first type of related posts are really just organization. I have three main categories on this site: How It Is, How It Works, and How To. I also have a million tags, like everyone else. If you wanted to read my thoughts on how things are, or rather why they are, you’d scroll through the category of “How It Works.” If you wanted to see everything I wrote about SVGs, you would check out my tag of ‘svg’ or possibly ‘images.’

    The point here is that categorization is a type of related posts. It’s entirely manual, but it’s the best way to say ‘These posts are like each other.’ And they have a fatal flaw. You see, if I wanted to read all the “How To” posts about SVGs, WordPress doesn’t easily cross relate. That is, I can’t list all the items in a category and a tag.

    Which is why we have …

    Related Posts Plugins

    There are two main types of plugins for this. There are the services, like Jetpack’s related posts, that scrape all your posts, toss them into a database, and use some complex algorithms to sort out what is and isn’t related. The other sort scan your posts locally and figure the same thing out.

    So which is better? Well. Jetpack requires you to trust Jetpack, or whatever service you pick, with your data. For some people, this can be a deal breaker. On the other hand, if you run it locally, you’re at the behest of how fast your site runs. For example, if it scans your posts live and you have, say, 300k posts, then that could be really slow. Or if it makes it’s own database table, how often is it going to update and cross relate?

    By the way, the 300k posts is not an exaggeration. That’s a site I looked at recently.

    Alertnative Relations

    There’s a secret third option, actually.

    I called it Semi Related Posts, and while I did it across post types, you could use the general logic. The concept is that instead of letting your site try and divine relations, you could manually connect them. It does require more upfront work, but cross relating posts by hand gives you the ultimate control.

    Of course, you’ll note that I did automate this as much as I could. I’m not crazy you know. If you can find a way to do that, maybe code a way to list 4 other posts in the same category and tags as this post, then you’ve automagically automated the simple.

    Until you hit that 300k post limit. Then you’ll have to rethink things again.