├── test ├── source │ └── _posts │ │ ├── 2011-01-16-post-3.textile │ │ ├── 2011-02-16-post-0.textile │ │ ├── 2011-08-16-post-1.textile │ │ ├── 2011-08-16-post-4.textile │ │ ├── 2011-08-16-post-5.textile │ │ ├── 2011-08-16-post-6.textile │ │ └── 2011-08-20-post-2.textile └── test_related_posts.rb ├── .gitignore ├── README.rdoc ├── LICENCE.rdoc └── _plugins └── related_posts.rb /test/source/_posts/2011-01-16-post-3.textile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/source/_posts/2011-02-16-post-0.textile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/source/_posts/2011-08-16-post-1.textile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/source/_posts/2011-08-16-post-4.textile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/source/_posts/2011-08-16-post-5.textile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/source/_posts/2011-08-16-post-6.textile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/source/_posts/2011-08-20-post-2.textile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore backup text files 2 | *~ 3 | 4 | # Ignore vi swap files 5 | *.swp 6 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = related_posts 2 | 3 | This is a {jekyll}[http://jekyllrb.com/] plugin that overrides the built in 4 | related_posts function to calculate related posts based on a posts' tags. 5 | 6 | == Installation 7 | To register it as a plugin, copy _plugins/related_posts.rb to the 8 | _plugins directory of your jekyll project. 9 | 10 | == How to use it 11 | The plugin replaces the functionality of site.related_posts so you 12 | can use it as follows: 13 | {% for post in site.related_posts %} 14 | {{ post.title }}
15 | {% endfor %} 16 | 17 | == Licence 18 | Copyright (c) 2011-2012, Lawrence Woodman 19 | This software is licensed under an MIT Licence. Please see the file, LICENCE.rdoc, for details. 20 | -------------------------------------------------------------------------------- /LICENCE.rdoc: -------------------------------------------------------------------------------- 1 | = The MIT Licence 2 | 3 | Copyright (c) 2011-2012 Lawrence Woodman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /_plugins/related_posts.rb: -------------------------------------------------------------------------------- 1 | require 'jekyll/post' 2 | 3 | module RelatedPosts 4 | 5 | # Used to remove #related_posts so that it can be overridden 6 | def self.included(klass) 7 | klass.class_eval do 8 | remove_method :related_posts 9 | end 10 | end 11 | 12 | # Calculate related posts. 13 | # 14 | # Returns [] 15 | def related_posts(posts) 16 | return [] unless posts.size > 1 17 | highest_freq = Jekyll::Post.tag_freq(posts).values.max 18 | related_scores = Hash.new(0) 19 | posts.each do |post| 20 | post.tags.each do |tag| 21 | if self.tags.include?(tag) && post != self 22 | cat_freq = Jekyll::Post.tag_freq(posts)[tag] 23 | related_scores[post] += (1+highest_freq-cat_freq) 24 | end 25 | end 26 | end 27 | 28 | Jekyll::Post.sort_related_posts(related_scores) 29 | end 30 | 31 | module ClassMethods 32 | # Calculate the frequency of each tag. 33 | # 34 | # Returns {tag => freq, tag => freq, ...} 35 | def tag_freq(posts) 36 | return @tag_freq if @tag_freq 37 | @tag_freq = Hash.new(0) 38 | posts.each do |post| 39 | post.tags.each {|tag| @tag_freq[tag] += 1} 40 | end 41 | @tag_freq 42 | end 43 | 44 | # Sort the related posts in order of their score and date 45 | # and return just the posts 46 | def sort_related_posts(related_scores) 47 | related_scores.sort do |a,b| 48 | if a[1] < b[1] 49 | 1 50 | elsif a[1] > b[1] 51 | -1 52 | else 53 | b[0].date <=> a[0].date 54 | end 55 | end.collect {|post,freq| post} 56 | end 57 | end 58 | 59 | end 60 | 61 | module Jekyll 62 | class Post 63 | include RelatedPosts 64 | extend RelatedPosts::ClassMethods 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/test_related_posts.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'minitest/mock' 3 | require 'minitest/spec' 4 | require 'jekyll' 5 | require_relative '../_plugins/related_posts' 6 | 7 | def createPost(site, id, date, tags) 8 | file_dir = File.expand_path(File.dirname(__FILE__)) 9 | post = Jekyll::Post.new( 10 | site, file_dir, 'source', "#{date}-post-#{id}.textile" 11 | ) 12 | post.define_singleton_method(:id) {id} 13 | post.define_singleton_method(:tags) {tags} 14 | post 15 | end 16 | 17 | describe Jekyll::Post do 18 | before do 19 | @site = MiniTest::Mock.new 20 | @posts = [] 21 | @posts << createPost(@site, 0, "2011-02-16", ['Retro', 'Programming']) 22 | @posts << createPost(@site, 1, "2011-08-16", ['Retro', 'Games']) 23 | @posts << createPost(@site, 2, "2011-08-20", ['Programming', 'Ruby']) 24 | @posts << createPost(@site, 3, "2011-01-16", ['Programming', 'C']) 25 | @posts << createPost(@site, 4, "2011-08-16", ['Off-topic', 'Funny', 'Story']) 26 | @posts << createPost(@site, 5, "2011-08-16", ['Programming', 'Games', 'C']) 27 | @posts << createPost(@site, 6, "2011-08-16", ['Retro', 'C']) 28 | end 29 | 30 | describe 'when given a post with nothing related' do 31 | it 'must return an empty array' do 32 | @posts[4].related_posts(@posts).must_equal [] 33 | end 34 | end 35 | 36 | describe 'when given a post with more than one related post' do 37 | it 'must return related posts in order of relationship strength and date' do 38 | @posts[5].related_posts(@posts).must_equal [ 39 | @posts[1], @posts[3], @posts[6], @posts[2], @posts[0] 40 | ] 41 | end 42 | 43 | it 'must return posts of equal relationship score in date order' do 44 | @posts[2].related_posts(@posts).must_equal [ 45 | @posts[5], @posts[0], @posts[3] 46 | ] 47 | end 48 | end 49 | 50 | end 51 | --------------------------------------------------------------------------------