My previous post on writing a simple related posts plugin was a popular one, and one reader wanted to take it further for a website he was working on. Well it was painfully obvious that my example was inadequate for doing any sort of fun stuff so I started adding bits and pieces to create a more robust widget. If you haven’t already, you might want to read the first post to get caught up.
This isn’t a true “plugin” in the sense you can activate it in WordPress, but more of a widget you can plug into your theme. This widget is going to grab posts based on tags and return specific info from the matching posts. This will be a good chance to see some of the guts of WordPress being used and how you might use them for your own projects.
Start Your Hacking Engines
We’ll first set up our widget.
<?php
//Start the related posts widget
function widget_related_posts() {
//code goes here
} //End widget and register the widget for use in WordPress
if ( function_exists('register_sidebar_widget') )
register_sidebar_widget(__('Related Posts'), 'widget_related_posts');
?>
Ok, nothing major, got our widget ready, I’ll be leaving this out of the code for the time being but everything I talk about below is contained where is says “code goes here”.
PHP Is Your Friend
Not really. Ok first things first, we need to get the tags for the current post. This is easy, we’ll use the function wp_get_post_tags(). Also set $post to global(we’ll use $wpdb later so I’ll set that to global too). In order to get the current post’s tags use $post->ID, if you want to make sure you are getting the tags use print_r(). The example below should make this all clear.
<?php
global $wpdb, $post;
$tags = wp_get_post_tags($post->ID);
//print_r ($tags);
Now we have a variable named $tags, which has…tags, genius I know. Now we need to do something with them.
<?php
global $wpdb, $post;
$tags = wp_get_post_tags($post->ID);
//print_r ($tags);
$taglist = "'" . str_replace("'",'',str_replace('"','',urldecode($tags[0]->term_id))) ."'";
$tagcount = count($tags);
if ($tagcount > 1) {
for ($i = 1; $i <= $tagcount; $i++) {
$taglist = $taglist . ", '" . str_replace("'",'',str_replace('"','',urldecode($tags[$i]->term_id))) . "'";
}
}
What happens here is we get some info from the tags for use in a query(more on that in a sec). The first $taglist is used if only one tag is present on the post. The second $taglist checks if there is more than one tag, then loops through each one, preparing them for whats next.
<?php
global $wpdb, $post;
$tags = wp_get_post_tags($post->ID);
//print_r ($tags);
$taglist = "'" . str_replace("'",'',str_replace('"','',urldecode($tags[0]->term_id))) ."'";
$tagcount = count($tags);
if ($tagcount > 1) {
for ($i = 1; $i <= $tagcount; $i++) {
$taglist = $taglist . ", '" . str_replace("'",'',str_replace('"','',urldecode($tags[$i]->term_id))) . "'";
}
}
$limit = "LIMIT 10";
$now = current_time('mysql',1);
$q = "SELECT DISTINCT pm.meta_id, pm.meta_key, pm.meta_value,
p.ID, p.post_title, p.post_date, p.comment_count, count(t_r.object_id) as cnt FROM $wpdb->term_taxonomy t_t,
$wpdb->term_relationships t_r, $wpdb->posts p, $wpdb->postmeta pm
WHERE t_t.taxonomy='post_tag' AND t_t.term_taxonomy_id = t_r.term_taxonomy_id
AND t_r.object_id = p.ID
AND (t_t.term_id IN ($taglist)) AND p.ID != $post->ID AND p.post_status = 'publish'
AND p.post_date_gmt < '$now' GROUP BY t_r.object_id
ORDER BY cnt DESC, p.post_date_gmt DESC $limit";
$related_posts = $wpdb->get_results($q);
// print_r ($related_posts);
There is a lot going on here. What I’ve added is a database query($q). This query is going to fetch the posts we want, and what we want from those posts. I’ll explain in short what is going on, but the WordPress codex would be a good place to learn more about this.
<?php $q = "SELECT DISTINCT pm.meta_id, pm.meta_key, pm.meta_value, p.ID, p.post_title, p.post_date, p.comment_count, count(t_r.object_id) as cnt FROM $wpdb->term_taxonomy t_t, $wpdb->term_relationships t_r, $wpdb->posts p, $wpdb->postmeta pm
This line selects distinct posts and what we want from those posts. The pm.* are getting the custom field information. The p.* are getting post info(ID, Title, Date, Comment Count). The “FROM” part is telling the query which databases to look for the information in.
<?php WHERE t_t.taxonomy='post_tag' AND t_t.term_taxonomy_id = t_r.term_taxonomy_id AND t_r.object_id = p.ID AND (t_t.term_id IN ($taglist)) AND p.ID != $post->ID AND p.post_status = 'publish' AND p.post_date_gmt < '$now'
The WHERE section is telling the database you only want posts that match "where this criteria is met". In this case we only want posts that match the tags associated with the current post. You could read this statement like so: Get posts that have tags matching those in $taglist but don't get the current post, make sure the post is published before the very second(ie don't get posts to be published in the future).
<?php GROUP BY t_r.object_id ORDER BY cnt DESC, p.post_date_gmt DESC $limit"; $related_posts = $wpdb->get_results($q);
We then group the posts by their ID in a descending order by date, but we limit the amount of posts to $limit(which is equal to 10 in this case). Then we run the query through the WordPress database, and set it to a variable called $related_posts.
Thats it, almost. Now we have an array with posts and their info and we are ready to print it out. But first a quick recap. We have gotten the tags for the current post, set them up for a database query, ran them through the database and got all the posts we want(and their specific info), then stuck them in an array. And it all looks like this.
<?php
global $wpdb, $post;
$tags = wp_get_post_tags($post->ID);
//print_r ($tags);
$taglist = "'" . str_replace("'",'',str_replace('"','',urldecode($tags[0]->term_id))) ."'";
$tagcount = count($tags);
if ($tagcount > 1) {
for ($i = 1; $i <= $tagcount; $i++) {
$taglist = $taglist . ", '" . str_replace("'",'',str_replace('"','',urldecode($tags[$i]->term_id))) . "'";
}
}
$limit = "LIMIT 10";
$now = current_time('mysql',1);
$q = "SELECT DISTINCT pm.meta_id, pm.meta_key, pm.meta_value,
p.ID, p.post_title, p.post_date, p.comment_count, count(t_r.object_id) as cnt FROM $wpdb->term_taxonomy t_t,
$wpdb->term_relationships t_r, $wpdb->posts p, $wpdb->postmeta pm
WHERE t_t.taxonomy='post_tag' AND t_t.term_taxonomy_id = t_r.term_taxonomy_id
AND t_r.object_id = p.ID
AND (t_t.term_id IN ($taglist)) AND p.ID != $post->ID AND p.post_status = 'publish'
AND p.post_date_gmt < '$now' GROUP BY t_r.object_id
ORDER BY cnt DESC, p.post_date_gmt DESC $limit";
$related_posts = $wpdb->get_results($q);
// print_r ($related_posts);
Almost Done, Outputting
Ok now we have our posts sitting in an array and we need to output all the info. I'm going to hard code in all the html for this example, but this is not always the best thing to do(Check out the widgets API for other ways). I'll start at the top:
<?php
//Start the related posts widget
function widget_related_posts() { ?>
<div class="widget related_posts">
<h3>Related Posts</h3>
<?php
global $wpdb, $post; ...
All I did there was add in a div (you could also use a unordered list here) and some class names to hook CSS into, also I added the title tag.
<?php $related_posts = $wpdb->get_results($q); //print_r ($related_posts); ?> <ul> <?php foreach ($related_posts as $related_post): //print_r ($related_post); ?>
This is the next step, after running our query take the array and do a "foreach" loop on it. Now we can output all the data we want.
<?php
<li><a href="<?php echo get_permalink($related_post->ID); ?>">
<?php echo $related_post->post_title ?></a></li>
<?php //get thumbnail (custom field)
$image = get_post_meta($related_post->ID, 'image', true);
if ( $image != '') {?>
<a href="<?php echo get_permalink($related_post->ID); ?>" rel="bookmark"
title="Permanent Link to <?php echo $related_post->post_title ?>">
<img src="<?php echo $image; ?>" alt="<?php echo $related_post->post_title ?>" /></a>
<?php } endforeach; ?>
</ul>
</div>
This is all the really important stuff here. I didn't use regular wordpress template tags here, I pulled the info straight out of the array. This example gives you a list with the title of the post and an image assigned to the custom field key "image". It also checks to see if anything is assigned to "image" so you don't get unexpected results.
The Whole Thing
Obviously you wouldn't want to use this for just related posts as there are plenty of plug-ins that can do that, but if you want specific results, the plug-ins are usually too general. The reader was going to use it to display related dvd's on posts, which is a perfect example of why you would want to build your own widget.
Here is the final code:
<?php
//Start the related posts widget
function widget_related_posts() { ?>
<div class="widget related_posts">
<h3>Related Posts</h3>
<?php
global $wpdb, $post;
$tags = wp_get_post_tags($post->ID);
//print_r ($tags);
$taglist = "'" . str_replace("'",'',str_replace('"','',urldecode($tags[0]->term_id))) ."'";
$tagcount = count($tags);
if ($tagcount > 1) {
for ($i = 1; $i <= $tagcount; $i++) {
$taglist = $taglist . ", '" . str_replace("'",'',str_replace('"','',urldecode($tags[$i]->term_id))) . "'";
}
}
$limit = "LIMIT 10";
$now = current_time('mysql',1);
$q = "SELECT DISTINCT pm.meta_id, pm.meta_key, pm.meta_value,
p.ID, p.post_title, p.post_date, p.comment_count, count(t_r.object_id) as cnt FROM $wpdb->term_taxonomy t_t,
$wpdb->term_relationships t_r, $wpdb->posts p, $wpdb->postmeta pm
WHERE t_t.taxonomy='post_tag' AND t_t.term_taxonomy_id = t_r.term_taxonomy_id
AND t_r.object_id = p.ID
AND (t_t.term_id IN ($taglist)) AND p.ID != $post->ID AND p.post_status = 'publish'
AND p.post_date_gmt < '$now' GROUP BY t_r.object_id
ORDER BY cnt DESC, p.post_date_gmt DESC $limit";
//echo $q;
$related_posts = $wpdb->get_results($q);
//print_r ($related_posts); ?>
<ul>
<?php
foreach ($related_posts as $related_post):
//print_r ($related_post); ?>
<li><a href="<?php echo get_permalink($related_post->ID); ?>">
<?php echo $related_post->post_title ?></a></li>
<?php //get thumbnail (custom field)
$image = get_post_meta($related_post->ID, 'image', true);
if ( $image != '') {?>
<a href="<?php echo get_permalink($related_post->ID); ?>" rel="bookmark"
title="Permanent Link to <?php echo $related_post->post_title ?>">
<img src="<?php echo $image; ?>" alt="<?php echo $related_post->post_title ?>" /></a>
<?php } endforeach; ?>
</ul>
</div>
<?php
} //End widget and register the widget for use in WordPress
if ( function_exists('register_sidebar_widget') )
register_sidebar_widget(__('Related Posts'), 'widget_related_posts');
A lot of this code is hacked up and stripped down from current plug-ins which I highly recommend looking into the code of if you are curious about WordPress and it's inner workings. Seeing how other more advanced people code will open you up to new ideas.
Update
One of my readers Martin read the first article about a related posts widget and wanted to take it further, the result of that conversation was this article. Once again Martin decided to take things even further and here are the results.
This example makes sure the widget title will not be shown if there are no related posts and instead replaces it with a google ad. This is a great use of real estate on a website.
<?php function widget_related_dvds() {
global $wpdb, $post;
$title = get_the_title($post->ID);
$taglist = "'" .
str_replace("'",'',str_replace('"','',urldecode($tags[0]->term_id)))
."'";
$tagcount = count($tags);
if ($tagcount > 1) {
for ($i = 1; $i <= $tagcount; $i++) {
$taglist = $taglist . ", '" .
str_replace("'",'',str_replace('"','',urldecode($tags[$i]->term_id)))
. "'";
}
}
$limit = "LIMIT 10";
$now = current_time('mysql',1);
$q = "SELECT DISTINCT pm.meta_id, pm.meta_key, pm.meta_value, p.ID, p.post_title, p.post_date, p.comment_count, count(t_r.object_id) as cnt FROM $wpdb->term_taxonomy t_t, $wpdb->term_relationships t_r, $wpdb->posts p, $wpdb->postmeta pm WHERE t_t.taxonomy='post_tag' AND t_t.term_taxonomy_id = t_r.term_taxonomy_id AND t_r.object_id = p.ID AND (t_t.term_id IN ($taglist)) AND p.ID != $post->ID AND p.post_status = 'publish' AND p.post_date_gmt < '$now' GROUP BY t_r.object_id ORDER BY cnt DESC, p.post_date_gmt DESC $limit";
$related_posts = $wpdb->get_results($q);
if (count($related_posts) > 0) {
print <h2 id="availability">DVD</h2>';
}
else {
print '<div class="adver">';
include(TEMPLATEPATH . '/google-ad-120x240.php');
print '</div><!-- end adhor -->';
}
?>
This example matches posts based on the Title and a certain category (in this case the DVD category). Note this is just the query portion of the script.
<?php
global $wpdb, $post;
$title = get_the_title($post->ID);
$limit = "LIMIT 10";
$now = current_time('mysql',1);
$q = "SELECT DISTINCT
p.ID,
p.post_title,
p.post_category,
p.post_date,
p.comment_count,
count(t_r.object_id) as cnt
FROM
$wpdb->term_relationships t_r,
$wpdb->posts p,
$wpdb->postmeta pm
WHERE p.post_title = '$title'
AND p.post_category = 'DVD'
AND t_r.object_id = p.ID
AND p.ID != $post->ID
AND p.post_status = 'publish'
AND p.post_date_gmt < '$now'
GROUP BY t_r.object_id
ORDER BY cnt DESC, p.post_date_gmt DESC $limit";
$related_posts = $wpdb->get_results($q);
Still not enough? It wasn't for Martin. Add this to the script and match posts based on tags and title!
<?php
$title = get_the_title($post->ID);
$related_posts = query_posts("category_name=DVD&tag=$title+$title");
There you go, three examples of this simple example made into something truly useful. If you have used this script or have a better way to do things, leave a comment!
13 Comments
Leave a CommentExcellent stuff! ;-)
14th Apr 2008
Hi Curtis,
Would it be possible to
- limit related posts to those from a certain category
- relate posts based on the_title instead of tags
?
15th Apr 2008
Martin,
Yes to both, at least in theory.
To limit posts to a certain category I would add another line to the database query. Something like AND post_category = ‘DVD’, this would exclude all other categories. I’m not sure how the database is structured off the top of my head so you would need to find what table and column that info is kept in. Let me know if you need help with that.
Another possible solution would be to use a conditional tag like if(is_category(‘DVD’) { //do stuff } Not sure if it would work but it would be easy to try out.
For the_title() I would do something similar. Use a database query to match the title with any other posts with the same title. Like WHERE post_title = p.post_title AND p.ID != $post->ID. All you need is to match the titles in the database.
15th Apr 2008
Great work!
Very useful
Thank you.
17th Jul 2008
Curtis – I’m trying to figure out how to also list the category of the related post, but apparently my knowledge of the db structure isn’t good enough. Can you point me in the right direction?
16th Oct 2008
Nicole,
Maybe something like this will work for you. Use it inside the foreach loop.
$foo = get_categories(); echo $foo[0]->cat_name;16th Oct 2008
Unfortunately that’s the sort of thing I’ve tried and had no luck with. If I put that in exactly, I see nothing (if I make it a list item, it displays a bullet with nothing next to it). The only way I get it to output anything is to echo $foo which gives me “array”. That’s why I’m thinking I need to grab the value directly out of the db somehow. This is definitely going to haunt me…
16th Oct 2008
It appears that if I use the the_category() within the foreach loop call it will return the category of the post I’m viewing, not of the related post. Other ideas?
16th Oct 2008
Nicole,
This code is long and ungraceful but it will get you an array of category IDs which you can then get the name of.
$foo = wp_get_post_categories($related_post->ID);echo print_r($foo);
$bar = get_cat_name($foo[0]);
echo $bar;
16th Oct 2008
Thank you! That was perfect. Here’s what I ended up using to produce something like: “News: Title of Related News Story”
ID); $cat_name = get_cat_name($category_list[0]); ?>
<a href=”ID); ?>”>
post_title ?>
16th Oct 2008
Thanks you so much for this great tutorial!
I’m trying to have both (same) category related posts and tag related posts so I used the code from your previous tut bellow the code from this tut but whenever I call both widgets the one makes the other dissapear! Obviously this is not the way to do it but how can I have both types of related posts together on the same page? Please help!
28th Dec 2008
Sarah,
Can’t be sure whats going on with out seeing the code, but look for anything that might be causing a conflict between the two functions, like something named the same.
28th Dec 2008
Thanks for replying so soon! Will do and let you know
29th Dec 2008