Showing Related Entries in Jekyll

February 19, 2019

The Problem

A common feature on many sites is a “related entries” type of function that shows, well, related articles at the end of a given post. The idea is to give your readers something useful to do once they get to the bottom of the page besides just scrolling up (and increase their time on site, page impressions, clicks on ads, etc. if your site cares about that sort of thing, which this one does not).

There’s countless related posts plugins for Wordpress but doing a search for something similar in Jekyll turns up… well, let’s just say that results are mixed.

The biggest challenge is that Jekyll, being a static site generator, is (for the most part) not capable of creating dynamic content. So while on a dynamically generated system like Wordpress it’s trivial to simply query the database with some joins across relevant fields and populate a random list of relevant related posts each time a page loads, in Jekyll this is not possible. As a result, most “related posts” type solutions for Jekyll tend to requires some (from my perspective) ugly compromises, usually one of the following:

  1. They just show the most recent 10 entries by default: that doesn’t fulfill the definition of “related posts”
  2. The require manual data entry of related posts in each entry’s front matter: laborious, error-prone, not particularly scalable
  3. They require you to install special ruby gems: feels yucky, overly complicated and makes moving between server environments annoying.
  4. They require specialised build commands: doesn’t work everywhere, slows build process
  5. And for most of the above, they also tend to be static: i.e. even if they show related posts, the posts they show remain fixed until the next time you build the site. Yuck.

The Requirements

From our perspective, any good “related posts” solution requires the following:

  1. Should show posts that are “related” in terms of having some meta-data connection to the currently showing post (i.e. don’t just list random posts or the most recent X posts, etc.)
  2. Should show different results (from within the set of matching related posts) every time a reader loads the page.
  3. Should not require the author to perform/specify any manual linkage when creating a new post
  4. Should scale automatically - i.e. as the count of entries/metadata grows, so too should the related post linkage.
  5. Should not require any special build commands or installation of any special ruby gems/jekyll plugins
  6. Should not show any duplicate entries
  7. Should not show the the current entry

The only solution that fulfills the requirements above is generally regarded to be some sort of client-side javascripting that reads from a data file of all posts on the site then manipulates the DOM to insert them dynamically per entry. Conceptually the solutions is simple, but the devil is in the details. After spending 30 minutes googling for a pre-built solution that satisfied all our requirements, we gave up and decided to just write our own.


The Solution

Generally speaking, there are four main components we need to make this work. The first two require a bit of work, the latter are trivial.

  1. A javascript-readable data file containing a mapping of all the posts on the site along with relevant information needed to create the “related” linking
  2. A javascript function that extracts whatever metadata to match on from the current entry, parses the data file to identify matching entries and inserts them into the DOM.
  3. Some sort of metadata in each entry to use to create a “related” linkage
  4. Modifying your entry template to include the JS file above (and give it somewhere to insert the related posts)
1. A Javascript readable data file of all our entries

Since Jekll doesn’t have a database, we need to create a data file of all the entries on the site in a javascript readable format. We will take advantage of the fact that when building a site, jekyll will process anything that has front matter on it. So in this case, we create a file (titled categorydata.js in our case, but call it whatever you want), and add some front matter so it gets generated each time jekyll builds the site.

In our case, we’ve decided that we will determine whether a post is “related” or not based on if it shares a category with the current entry (more on that in a second below). So inside of this file we loop all the categories in the site, then loop all the entries in each category, and then output it in a JSON object, assigned to a variable titled categoryData. We’ll be reading this variable in our main javascript function in a minute.

Generally speaking, unless you’re doing something really crazy, you should always have a url and a title for your posts. In our case, we always specify two additional pieces of metadata in the front matter when creating a post: category and snippet.

For us, this doesn’t count as “extra manual work” since we consider it fundamental to getting the site to work how we want it (even in the absence of a related posts), but your mileage may vary. snippet can of course be omitted if you don’t want to use a snippet, but if you omit category from the front matter of your posts, then you’ll need to replace it with something else (both in each entry and in the code below) to use to create the related linkage.

---
layout: null
title: categorydata.js
---
// JSON file containing entries+urls by category. Generated by Jekyll
{% assign sorted_cats = site.categories | sort %}
{% for category in sorted_cats %}

		{% if forloop.first %}
			var categoryData = {
		{% endif %}

		"{{category[0] | lowercase}}": [
	
			{% assign sorted_posts = category[1] | reversed %}
			{% for post in sorted_posts %}
				{% if forloop.last %}
					{
						"url": "{{ post.url }}",
						"title": "{{ post.title }}",
						"snippet": "{{ post.snippet }}"
					}
				{% else %}
					{
						"url": "{{ post.url }}",
						"title": "{{ post.title }}",
						"snippet": "{{ post.snippet }}"
					},
				{% endif %}
			{% endfor %}


		{% if forloop.last %}
			]};
		{% else %}
			],
		{% endif %}
{% endfor %}
2. A javascript function to extract the current post metadata, parse the data file and create the linkage

This is where the heavy lifting takes place. The script runs when an individual post page is loaded and does three things.

  1. Grabs all the categories assigned to the current entry from the DOM. You’ll need to modify the jQuery selector to reflect however you’ve chosen to create your markup on the entry template
  2. Loops through the categoryData JSON object we’ve created above and grabs information on all entries that match any of the categories shared by the current entry.
  3. Performs a bit of de-duplication while doing the step above to remove duplicate entries/links to the current entry
  4. Randomly chooses X number of related entries from the deduplicated array of matching entries, and then writes out the relevant HTML and inserts it into the DOM.

A few notes:

  1. We happen to be using jQuery already on our site so this script leverages some jQuery selectors to work. You absolutely do not need jQuery to do this, it can be written in pure javascript, but of course you’ll need to rewrite the way the selectors work below.
  2. This is most assuredly not the best or most efficient way to accomplish this task. We’re (obviously) not javascript geniuses, we know just enough to be dangerous. Comments to improve this script most welcome.
  3. You’ll need to modify the last section (where the related post HTML is being created and written to the DOM) to reflect however you want your particular markup/template to work.
$(document).ready(function(){

	/*
		Make sure you've included the script containing the
		categoryData var somewhere before this script
	*/

	// Helper method to read size (number of entries) of each category
	Object.size = function(obj) {
		var size = 0, key;
		for (key in obj) {
			if (obj.hasOwnProperty(key)) size++;
		}
		return size;
	};

	
	// Set up initial variables
	var postCategories = new Array;
	var relatedPosts = new Object();
	var postsToShow = 3;			// How many related posts to show
	

	// Grab all the categories from the DOM of the current post.
	// Change jQuery selector to reflect your template markup
	$(".post-meta > a").each(function(){
		postCategories.push($(this).text());
	});

	
	// Get info on all entries that match the category of the current post.

	var len = postCategories.length;	// Increment for relatedPosts index
	var x = 0;
	var uniqueUrlArray = [window.location.pathname];	// prevent dupes
	for (var i = 0; i < len; i++) {
		var category = postCategories[i];
		var size = Object.size(categoryData[category]);

		// Push each entry for the current category into the relatedPosts
		for (var j=0; j<size; j++) {
			// Only push unique entries into the array
			if (uniqueUrlArray.indexOf(categoryData[category][j]['url']) === -1) {
				uniqueUrlArray.push(categoryData[category][j]['url']);
				relatedPosts[x] = categoryData[category][j];
				x++;
			}
		}
	}

	
	// Handle case when there's not enough related posts to show
	var relatedPostsLength = Object.size(relatedPosts);
	if (postsToShow > relatedPostsLength) {
		postsToShow = Object.size(relatedPosts);
	}


	// Grab three unique random indices from within all the relatedPosts
	var uniqueIndexArray = [];
	while(uniqueIndexArray.length < postsToShow) {
		var r = Math.floor(Math.random()*relatedPostsLength);
		if(uniqueIndexArray.indexOf(r) === -1) uniqueIndexArray.push(r);
	}


	// Loop the array and write to DOM
	for (var k = 0; k < uniqueIndexArray.length; k++) {
		var index = uniqueIndexArray[k];
		var content = '<li><a href="' + relatedPosts[index]['url'] + '">';
		content = content + '<span>' + relatedPosts[index]['title'] + '</span>';
		content = content + relatedPosts[index]['snippet'];
		content = content + '</a></li>';
		$(".related-posts ul").append(content);
	}


});
3. Metadata in each entry to make the linkage work.

As mentioned, at the top of each entry we need to provide some metadata for the “linkage” to occur on. We already use categories in our site1 so that is always normally specified at the top of each post. We use hyphenation and no spaces/quotes in our categories, since Jekyll normally delimits items in thefront matter by space. The snippet metadata is optional, you could remove this if you don’t want to use that (just make the appropriate modifications to the two other pieces of code above)

An example of a sample entry might look like this:

---
layout: post
title:  "Summer Sunsets over Mount Tamalpais"
date:   2018-08-24 12:30:00 +0900
categories: travel san-francisco california
snippet: "Watching the sun set over Mount Tamalpais"
---

(Post content goes here)
4. Modify entry template to include the JS files and give somewhere to insert the related posts

There’s four main changes to make in your entry template:

  1. Make sure to include the categorydata.js file generated in step #1 above in the header
  2. Make sure to include the main javascript file (main.js in my case) in the header after it.
  3. Writing the categories of the entry into the page so the script from #2 above can pick it up.
  4. Having some place in the markup for the related entries to be inserted into.

In our case, a simplified version of our post template looks like this:

<html>
<head>
	<meta charset="utf-8">
	<title>Your Site Title Here</title>
	<link rel="stylesheet" href="/css/main.css">

	<!-- link the categorydata.js and the main.js files -->
	<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
	<script language="javascript" type='text/javascript' src='/scripts/categorydata.js'></script>
	<script language="javascript" type='text/javascript' src='/scripts/main.js'></script>

</head>
<body>

	<h1 class="entry-title">{{page.title}}</h1>
	<h2>{{page.date | date: "%B %d, %Y" }}</h2>

	<!-- Post Content -->
	{{ content }}


	<h2>Related Posts</h2>
	<div class="related-posts">
		<ul>
		<!-- Related posts are inserted here via Javascript -->
		</ul>
	</div>


	<!-- List out all categories of the current post for the JS file to read in -->
	<p class="post-meta">
		{% for category in page.categories %}
		<a href="/categories/#{{category}}" />{{category}}</a>
		{% endfor %}
	</p>
</body>
</html>

And that (phew) is it!

  1. And we would normally do so even if we weren’t to implement a “related posts” type of function so this isn’t any extra work. Additionally categories provides the right level of granularity in our opinion - plentiful and varied enough that there will be sufficient entries to populate the related entries data set, but not so many variations (as compared to a tag) that we can’t remember which categories exist or that we mispell them or end up with sparse data sets (as might happen with tags, if you can’t remember which tags you use you can end up with tags that only have 1~2 entries ever assigned to them which means your function will always show the same related entries) 

Keep Reading

Related Posts

⤒ Back to top

Permalink