How To Add Weighted Related Posts

by on March 25, 2011

Providing related post links is both a convenience for your readers and a small boost for your search engine optimization. As such, there are a number of WordPress plugins that will add related post links at the end of our articles. The best ones (for performance) invoke this task from the Dashboard.

But, what if you want a related post capability with your theme by default? Or, what if you have too many plugins running already? Today, we have a function that you can call from your theme's single.php the same way that you display tag and category links.

It “discovers” related posts by matching tags. Plus, our function is unique in that it weights the priority of related posts by the number of tags (keywords) that are matched to the current post (article). This way, the more relevant related posts (the ones with the most tag matches) are displayed first in the list. And, our function also offers a facility to limit the number of returned matches and to exclude the posts that you specify.

Here's an actual usage example from a site. Put this line (or one similar) in your theme's single.php file and then put the functions below, wcs_related_posts() and wcs_get_related_posts() in the same theme's functions.php file.

Code: PHP (plus WordPress)

<p style="margin-bottom:8px;"><span class="icon_related"><?php wcs_related_posts($post->ID); ?></span></p>

One of the cool features of this functionality is that the list of returned links is alphabetical WITH more relevant links at the beginning of the list. For example, if three posts each matched a single tag (keyword) for the current post, the output list would simply be alphabetical by post title.

However, if one post matched 4 tags and another matched 3 tags. They would be moved to the front of the list, respectively. Thus, the item with 4 matches would be first, the one 3 matches would be second, and the ones with a solitary match would follow in alphabetical sequence.

Like many internal WP functions, we have two versions our function. The first one, wcs_related_posts(), calls the workhorse function and echos the output. Let's look at the available parameters.

The first one, $post_id, will typically be $post->ID when called from within the loop. This is most likely the way you'll use it. And, generally, this will be the only value you need to set.

$match_limit is the maximum number of matches to output.

$exclude_list is a comma-separated list of post ID's and/or slugs that you want to exclude. When used in your single.php file, this will exclude the items in the list from all posts. You may want to set this value if you build a list of related posts outside normal loop processing.

$before_item is the HTML that will be displayed before each item in the list.

$after_item is the HTML that will be displayed after each item in the list.

$before_content is the HTML that's displayed before the entire list. By default, it's value is simply “Related Articles: “.

$after_content is the HTML that's displayed after the entire list.

$none_msg is the message to display if there are no articles with matching tags found.

$none_to_cats is a toggle to switch to posts that match the top-level category for this post … if no tag matches are found.

Code: PHP (plus WordPress)Function: wcs_related_posts()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function wcs_related_posts(
        $post_id=1,
        $match_limit=5,
        $exclude_list='',
        $before_item='',
        $after_item=', ',
        $before_content='Related Articles: ',
        $after_content='',
        $none_msg='(no keyword matches)',
        $none_to_cats=1
        )
{
    echo wcs_get_related_posts($post_id, $match_limit, $exclude_list, $before_item,
        $after_item, $before_content, $after_content, $none_msg, $none_to_cats);
}

Here's the workhorse function. Put both of these and the third support function (below this one) in your theme's functions.php file. Then, call wcs_related_posts() in your single.php file as described above.

Code: PHP (plus WordPress)Function: wcs_get_related_posts()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
function wcs_get_related_posts(
        $post_id=1,
        $match_limit=5,
        $exclude_list='',
        $before_item='',
        $after_item=', ',
        $before_content='Related Articles: ',
        $after_content='',
        $none_msg='(no keyword matches)',
        $none_to_cats=1
        )
{
    // reference: http://codex.wordpress.org/Function_Reference/WP_Query
 
    // this function should be called from within the loop; but not mandatory
    // $none_msg is displayed if there are no articles with matched tags (keywords)
    // output list is alphabetical, but weighted toward articles with multiple tag matches
    // plugins offer greater functionality, but this can be part of a single.php file
 
    // $exclude_list should not used in single.php unless you want to permanently
    // exclude certain posts; it's best used outside the loop for a list of related posts
 
    // init
    global $post, $wpdb;
    $backup = $post;
    $posts_list = array();
    $links = array();
    $exc_array = array();
    $output = '';
    $i = 0;
    $link_max = 0;
    $exc_array_size = 0;
    $max_matches_per_tag = -1; // -1 = match ALL articles with each tag
 
    // prep for processing
    $tags = wp_get_post_tags($post_id);
    $tag_count = sizeof($tags);
 
    // begin processing
    if ($tag_count > 0)
    {
        for ($i = 0; $i < $tag_count; $i++)
        {
            $this_query = null;
            $this_tag = $tags[$i]->term_id;
            $args = array
            (
                'tag__in' => array($this_tag),
                'post_type' => 'post',
                'post_status' => 'publish',
                'post__not_in' => array($post_id), // ignore current
                'posts_per_page' => $max_matches_per_tag,
                'ignore_sticky_posts' => 1
            );
            $this_query = new WP_Query($args);
 
            if($this_query->have_posts())
            {
                while ($this_query->have_posts())
                {
                    $this_query->the_post(); // get data for this post
                    $posts_list[] = $post->post_name; // slug
                }
            }
        }
 
        if (sizeof($posts_list) > 0)
        {
            // remove excluded items
            if ($exclude_list != '')
            {
                $exclude_list = preg_replace('/\s+/', '', $exclude_list);
                $exc_array = explode(',', $exclude_list);
                $exc_array_size = sizeof($exc_array);
                // check for page id's instead of slugs (slows the process)
                for ($i = 0; $i < $exc_array_size; $i++)
                {
                    if (is_numeric($exc_array[$i]))
                    {
                        $exc_array[$i] = $wpdb->get_var("SELECT post_name FROM $wpdb->posts WHERE ID = '" . $exc_array[$i] . "' and post_status='publish' LIMIT 1");
                    }
                }
                // now remove them and resquence
                $posts_list = array_diff($posts_list, $exc_array);
                $posts_list = array_values($posts_list);
            }
 
            // sort alphabetically, but give duplicates highest priority
            rsort(&$posts_list, SORT_STRING);
            $posts_list_sorted = array_count_values($posts_list);
            arsort(&$posts_list_sorted, SORT_NUMERIC);
 
            // begin output string
            $output = $before_content;
 
            // now construct the links
            foreach ($posts_list_sorted as $key => $val)
            {
                $post_id = $wpdb->get_var("SELECT ID FROM $wpdb->posts WHERE post_name = '" . $key . "' and post_status='publish' LIMIT 1");
                $permalink = get_permalink($post_id);
                $title = get_the_title($post_id);
                $links[] = '<a href="' . $permalink . '" rel="bookmark">' . $title . '</a>';
            }
 
            // continue output string
            $link_max = sizeof($links);
            if ($link_max > $match_limit) {$link_max = $match_limit;}
            for ($i = 0; $i < $link_max; $i++)
            {
                $output .= $before_item . $links[$i];
                if ($i < $link_max - 1)
                {
                    $output .= $after_item;
                }
                else
                {
                    // omit possible trailing comma
                    $output .= str_replace(',', '', $after_item);
                }
            }
            $output .= $after_content;
        }
        else
        {
            if ($none_to_cats == false)
            {
                $output = $before_content . $none_msg . $after_content;
            }
            else
            {
                $output = $before_content;
                $output .= wcs_get_category_posts_list(
                        $post_id, $match_limit,
                        $before_item, $after_item,
                        $none_msg);
                $output .= $after_content;
            }
        }
    }
    else
    {
        if ($none_to_cats == false)
        {
            $output = $before_content . $none_msg . $after_content;
        }
        else
        {
            $output = $before_content;
            $output .= wcs_get_category_posts_list(
                    $post_id, $match_limit,
                    $before_item, $after_item,
                    $none_msg);
            $output .= $after_content;
        }
    }
 
    // exit
    $post = $backup;
    wp_reset_query();
    return $output;
}
    /**********************************************************************
     Copyright © 2011 Gizmo Digital Fusion (http://wpCodeSnippets.info)
     you can redistribute and/or modify this code under the terms of the
     GNU GPL v2: http://www.gnu.org/licenses/gpl-2.0.html
    **********************************************************************/
}

If there is a value in the function that you might want to customize, it would be $max_matches_per_tag on line 33. This is the number of matching posts that any one tag can return.

In lines 40-65, we query the database for matching articles. In line 42, we start iterating through each tag of the current post to gather others posts that share this particular tag. In line 62, we add the slug for each matching post to the $posts_list array. We used an array here because we're about to take advantage of PHP's very useful array manipulation functions.

In lines 67-86, we remove any excluded items from our array of slugs. This extraction is accomplished on line 84 with PHP's array_diff() function. But, this function leaves gaps in the array … so we then invoke array_values() to compact the array.

Next, we utilize a combo of three array manipulation functions that weight our slug array and sort it. In line 89, we reverse sort this array. In line 90, we construct another array that contains both keys and values. The keys are the slugs and the values are the number of occurrences of that particular slug in the original array. Cool, huh?

Now, we do a reverse numeric sort which puts the tags with the most occurences at the beginning of the array. Upon completion, with the exception of the weighting, everything's alphabetized.

In lines 96-103, we iterate through our sorted array constructing anchor links to the matching posts. Finally, from line 105 on, we build the HTML output string limiting the result to the requested number of links.

If $none_to_cats is set to true, we call the following function. In short, if there are no related posts using the tags/keywords, we use the first top-level category/topic instead.

Code: PHP (plus WordPress)Function: wcs_get_category_posts_list()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
function wcs_get_category_posts_list(
        $post_id=1,
        $match_limit=5,
        $before_item='',
        $after_item=', ',
        $none_msg='(no keyword matches)'
        )
{
    // init
    global $post, $wpdb;
    $backup = $post;
    $output = '';
 
    // get list of categories for this post
    $cat_array = get_the_category($post_id);
    $cat_count = sizeof($cat_array);
    if ($cat_count == 0) {return $none_msg;}
 
    // get first top-level category
    $first_top_cat_id = 0;
    for($i = 0; $i <= $cat_count; $i++)
    {
        if ($cat_array[$i]->parent == 0)
        {
            $first_top_cat_id = $cat_array[$i]->term_id;
            break;
        }
    }
    if ($first_top_cat_id == 0) {return $none_msg;}
 
    // get posts for the first top-level category
    $args = 'cat=' . $first_top_cat_id;
    $args .= '&posts_per_page=' . $match_limit;
    $args .= '&post_status=publish&orderby=post_date&order=DESC';
    $this_query = new WP_Query($args);
 
    // construct list of posts for the first top-level category
    if($this_query->have_posts())
    {
        $counter = 1;
        $this_count = $this_query->post_count;
        while ($this_query->have_posts())
        {
            // construct single item
            $this_query->the_post(); // get data for this post
            if ($post->ID != $post_id) // omit current post
            {
                $output .= $before_item;
                $output .= '<a href="' . get_permalink() . '">';
                $output .= get_the_title() . '</a>';
                if ($counter != $this_count) {$output .= $after_item;}
                // strip a possible final comma in the list
                else {$output .= str_replace(',', '', $after_item);}
            }
            $counter++;
        }
    }
    else
    {
        $output = $none_msg;
    }
 
    // exit
    $post = $backup;
    wp_reset_query();
    return $output;
}

This one operates quite similarly to the primary function, except that it works with categories instead of tags. In line 15, we use get_the_category() to build an array of all the categories associated with this post. Then in lines 19-29, we get the first top-level category in this array.

Finally, beginning at line 31, we get a list of posts that match this category. Likewise, we return the list of links to the calling function, wcs_get_related_posts().

As a result, every post should have some related posts to display … either related by tag (the priority) or by category (the fallback). And, as mentioned above, the tag list is weighted in sequence with posts that match the most tags having priority.

Share This Article: “How To Add Weighted Related Posts”

(Also Available: Press CTRL+D to Bookmark this Page)

Comments

Share Your Thoughts  10 Responses to “How To Add Weighted Related Posts”
  1. 1
    Jim says:

    Do you plan on making this one into a plugin too?

  2. 2
    Luke America says:

    Yes, Jim. There’s a high probability of doing just that.

  3. 3
    Rick Taylor says:

    This snippet works better than any of the plugins I’ve tried for adding related post links. Awesome dude.

  4. 4
    Doug Sloan says:

    Just what I needed for my blog. :)

  5. 5
    patrik eriksen says:

    very useful similar posts code!

  6. 6
    Live Cricket says:

    This works perfectly on my site. Thanks! :)

  7. 7
    M-J Jones says:

    Hi,Thanks for this tip. Will try it this week-end.Wonder whether you used a plugin to show tags, keywords, etc. at the bottom of your post. And if so, which one.I use lots of keywords and right now they clutter the title of posts. Would like to group them at the end of the single.php page just like you do.By curiosity, did you code your social share section of did you use a plugin?Thanks in advance for your reply and wish you a pleasant we.

  8. 8

    Apartment – de luxe, spacious latitude (XVI century), in the nineteenth century, the term in use accustomed to to refer to a splendour apartment or group of rooms intended in return entire user.
    Condo (from Lat. Con-= ‘patronage’, province = ‘sovereignty’) is a proprietary people apartment in a building housing that is self-satisfied and commonly voluptuary accommodation.

    When the condo is located on the outstrip astound, often with the largest terrace is a penthouse.

    Stretched out – in accordance with the normative demarcation set outside in the Reverend of Infrastructure dated 12 April 2002 on the technical conditions to be met by buildings and their location (Gazette of Laws No. 75 item. 690 with later. D..) – In force since December 16, 2002 – a habitation unit and ancillary convenience, with away coming, independent compartments swarming construction, which allows constant residency of people and conduct an competent household.

    Bulk the forms of residential lodgings is renowned around:

    Cooperative (put into Lofty Warszawa mending through the cooperative protection for members of the cooperatives),
    utilities (built with cabbage from the metropolitan budget, these are social housing and intervention),
    to save purchase and slit,
    Socio-rent (implemented by the Society for Social Shield in favour of the money the Federal Protection Wherewithal)
    lone,
    company.

  9. 9
    Maraacousia says:

    okulary przeciwsloneczneZaczęły się już do nas letnie dni, w związku z tym na dworze panuje śliczna słoneczna pogoda od trzech dni. Wszyscy Polacy od kilku miesięcy z nadzieją oczekiwali nadejścia letnich tygodni. Afrykańską pogodę cenią nie jedynie najmłodsi, lecz również i wielu ich rodziców. Wyśmienita zabawa na świeżym powietrzu owocuje, że przeważnie czekamy z wytęsknieniem nadejścia wakacji. W każdej chwili w czasie wczasów musimy myśleć o tym, abyśmy zachowywali się w bezpieczny sposób. Powinno się pamiętać o tym, ażeby smarować własne ciało przeznaczonymi w tym celu kremami zabezpieczającymi nas przed szkodliwym promieniowaniem. W ciągu upalnych godzin wskazane jest nosić okulary przeciwsłoneczne, bowiem wyłącznie za ich sprawą nasze oczy będą należycie zabezpieczone przed promieniowaniem. Nasze okulary muszą być porządnie spolaryzowane, aby chronić Twoje oczy przed promieniowaniem. W każdym roku nietrudno zauważyć, że przekształcają się trendy dotyczące strojów kąpielowych damskich i męskich. W te wakacje do łask powracają jednokolorowe stroje bikini w kolorze szarym, zielonym i niebieskim. W obecne wakacje nadzwyczaj modne będą duże okulary przeciwsłonczne oraz również małe dodatki. Panom proponujemy przede wszystkim fascynujące szorty, a również czarną, obcisłą koszulkę jednolitą. Znakomita zabawa, a także niezapomniane momenty muszą być jednak znacznie ważniejsze.

  10. 10
    cisyTrairee says:

    mielno W naszym kraju znajduje się dużo fantastycznych miejscowości nadmorskich, jakie na prawdę musisz zwiedzić. Zalicza się do nich Mielno. Mielno to cudowne miasteczko położone nad urokliwym Morzem Bałtyckim. Polacy niezwykle często na przestrzeni urlopu wyruszają do Mielna wraz ze swoimi rodzinami. W Mielnie przyszykowanych jest dziesiątki niespodzianek dla podróżników pochodzących z całego państwa oraz z zagranicy. Mielno ze względu na wiele atrakcji dla dzieci jest doskonałym wyborem na urlop rodzinny, ale również i na kolonie. Noclegi Mielno zaliczają się do jednych z nad wyraz lubianych naszym kraju, a również nad morzem Bałtyckim. Letnie tygodnie są tym na co niesłychanie dużo osób czeka przez resztę roku. Wyjeżdżając na urlop mamy przede wszystkim nadzieję na to, że będziemy się kapitalnie bawić, a również odpoczniemy w miły sposób. Mielno jest nazywane najgorętszym Polskim kurortem, ponieważ istnieje tutaj bardzo dużo pubów oraz także miejsc, w jakich można potańczyć przy ciekawych rytmach. Dobre oraz także niedrogie noclegi Mielno mamy okazję znaleźć na dziesiątkach campingów, które lokują się prawie że przy samej plaży. Jest to niezwykle dobra oferta dla ludzi z mniejszą ilością pieniędzy, więc dla młodych ludzi. Nietrudno zauważyć pewną tendencję, która wywołuje, że nadbałtyckie wybrzeże jest co raz bardziej atrakcyjne dla cudzoziemnców.

Share Your Thoughts

(Some editor features are restricted unless you're logged in.)

(When replying to a specific comment, your browser may require Shift+Enter instead of just Enter.)


(get a gravatar)


Notify me of followup comments via e-mail. You can also subscribe without commenting.