Question
How to output tag related posts first, then IF there are less than 4 tag related posts, fill the rest of the 4 spots with category related posts?
Scenario
Sometimes a post has very few tags, or the tags it is labeled with have very fews posts. When outputting “related posts by tag,” the area is either very sparse with 1 or 2 posts, or outright empty.
To solve this it would be good to show posts from related categories, if there are not enough tag related posts to satisfy posts_per_page => X
.
So, something like this:
Scenario A — if more than 4 tag-related posts exist, then:
Related Posts:
Show the below posts:
1. tag-related post #1
2. tag-related post #2
3. tag-related post #3
4. tag-related post #4
Do Not show the below posts:
5. tag-related post #5
6. tag-related post #6
7. tag-related post #7
...
Scenario B — if only 2 tag-related posts exist, then:
Related Posts:
Show the below posts:
1. tag-related post #1
2. tag-related post #2
3. category-related post #1
4. category-related post #2
Do Not show the below posts:
5. category-related post #3
6. category-related post #4
7. category-related post #5
...
What I’ve tried
The tax query I am using:
// start of the tax_query arguments
$args = array( 'posts_per_page'=>4, 'post__not_in' => array($post->ID), 'tax_query' => array( 'relation' => 'OR' ) );
// get current post tags
$tags = wp_get_object_terms( $post->ID, 'post_tag', array( 'fields' => 'ids' ) );
if ( !empty( $tags ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'post_tag',
'field' => 'id',
'terms' => $tags
);
}
// get current post categories
$categories = wp_get_object_terms( $post->ID, 'category', array( 'fields' => 'ids' ) );
if ( !empty( $categories ) ) {
$args['tax_query'][] = array(
'taxonomy' => 'category',
'field' => 'id',
'terms' => $categories
);
}
// the query
$related_query = new WP_Query( $args );
As I understand it, that tax query says “get posts that are in the same categories, then get posts that are in the same tags, then output posts until 4 are on screen.”
Yet it keeps outputting category posts first (of which there are many) which satisfies the 4 posts on screens rule and leaves out the most important tag-related posts. I’ve tried moving the code around, using AND
instead of OR
, which didn’t work and made no sense to me anyways.
I’ve also seen these posts: WordPress – producing a list of posts filtered by tag and then category and WordPress query posts by tag and category, but they are about outputting a list of posts that are filtered by tag AND category. I need posts first related by tag, and if 4 or more, then just output those top 4. If less than 4, then output up to as many category-related posts to meet the 4 posts criteria.
Clearly I am misunderstanding the query and/or the problem, so any help would be appreciated.
SOLUTION
Peter’s solution works perfectly — thank you Peter!
I’ve further built on it a bit for my own purposes, and using your awesomely commented code, I was able to also Exclude “Recent Posts” from this query — woohoo!
You cannot do this in one query, this is a bit too advanced for what WP_Query
can do at this point in time. We will need to run at least two (I’m going to use 3) queries here to achieve what you want
FIRST QUERY
The first query will query for posts according to post_tag
, that will also exclude the current post. We will only get post ID’s
SECOND QUERY
This query will handle the “fill-up” posts which will come from the category
taxonomy. For this query to be successful, we will need the following
-
Exclude the current post and the posts from the first query to avoid duplicate posts
-
Get the amount of posts in the first query, subtract that from 4, and then use that diffrence to set the amount of posts that should be retrieved by the this query
We will also just get post ID’s here
THIRD QUERY
In order to keep the integrity from the query object, we will combine all the ID’s from the previousquery and query the final post objects. You might think this is expensive, but it is not. Because the first two queries uses get_posts()
and only get post ID’s, they are really super fast. By querying only ID’s, we also do not updates caches which makes these queries even faster
SOLUTION
I prefer creating a function for such big pieces as code as it keeps my templates clean. Please note, the following is untested and requires PHP 5.4
function get_max_related_posts( $taxonomy_1 = 'post_tag', $taxonomy_2 = 'category', $total_posts = 4 )
{
// First, make sure we are on a single page, if not, bail
if ( !is_single() )
return false;
// Sanitize and vaidate our incoming data
if ( 'post_tag' !== $taxonomy_1 ) {
$taxonomy_1 = filter_var( $taxonomy_1, FILTER_SANITIZE_STRING );
if ( !taxonomy_exists( $taxonomy_1 ) )
return false;
}
if ( 'category' !== $taxonomy_2 ) {
$taxonomy_2 = filter_var( $taxonomy_2, FILTER_SANITIZE_STRING );
if ( !taxonomy_exists( $taxonomy_2 ) )
return false;
}
if ( 4 !== $total_posts ) {
$total_posts = filter_var( $total_posts, FILTER_VALIDATE_INT );
if ( !$total_posts )
return false;
}
// Everything checks out and is sanitized, lets get the current post
$current_post = sanitize_post( $GLOBALS['wp_the_query']->get_queried_object() );
// Lets get the first taxonomy's terms belonging to the post
$terms_1 = get_the_terms( $current_post, $taxonomy_1 );
// Set a varaible to hold the post count from first query
$count = 0;
// Set a variable to hold the results from query 1
$q_1 = [];
// Make sure we have terms
if ( $terms_1 ) {
// Lets get the term ID's
$term_1_ids = wp_list_pluck( $terms_1, 'term_id' );
// Lets build the query to get related posts
$args_1 = [
'post_type' => $current_post->post_type,
'post__not_in' => [$current_post->ID],
'posts_per_page' => $total_posts,
'fields' => 'ids',
'tax_query' => [
[
'taxonomy' => $taxonomy_1,
'terms' => $term_1_ids,
'include_children' => false
]
],
];
$q_1 = get_posts( $args_1 );
// Count the total amount of posts
$q_1_count = count( $q_1 );
// Update our counter
$count = $q_1_count;
}
// We will now run the second query if $count is less than $total_posts
if ( $count < $total_posts ) {
$terms_2 = get_the_terms( $current_post, $taxonomy_2 );
// Make sure we have terms
if ( $terms_2 ) {
// Lets get the term ID's
$term_2_ids = wp_list_pluck( $terms_2, 'term_id' );
// Calculate the amount of post to get
$diff = $total_posts - $count;
// Create an array of post ID's to exclude
if ( $q_1 ) {
$exclude = array_merge( [$current_post->ID], $q_1 );
} else {
$exclude = [$current_post->ID];
}
$args_2 = [
'post_type' => $current_post->post_type,
'post__not_in' => $exclude,
'posts_per_page' => $diff,
'fields' => 'ids',
'tax_query' => [
[
'taxonomy' => $taxonomy_2,
'terms' => $term_2_ids,
'include_children' => false
]
],
];
$q_2 = get_posts( $args_2 );
if ( $q_2 ) {
// Merge the two results into one array of ID's
$q_1 = array_merge( $q_1, $q_2 );
}
}
}
// Make sure we have an array of ID's
if ( !$q_1 )
return false;
// Run our last query, and output the results
$final_args = [
'ignore_sticky_posts' => 1,
'post_type' => $current_post->post_type,
'posts_per_page' => count( $q_1 ),
'post__in' => $q_1,
'order' => 'ASC',
'orderby' => 'post__in',
'suppress_filters' => true,
'no_found_rows' => true
];
$final_query = new WP_Query( $final_args );
return $final_query;
}
You can now use the function as follow in your single template
$query = get_max_related_posts();
if ( $query ) {
while ( $query->have_posts() ) {
$query->the_post();
echo get_the_title() . '</br>';
}
wp_reset_postdata();
}
FEW NOTES
-
The defaults are already set to post_tag
, category
and 4
for the three parameters respectively, so you do need need to pass any values to the function when calling it
-
If you need to swop the taxonomies around or set different taxonomies or set the posts per page to anything else than 4, simply pass them in the correct order to the function
$query = get_max_related_posts( 'tax_1', 'tax_2', 6 );