Translated by readers to: Russian

You know how some blogs have a section after reading an article that says "Related Posts" or "You might also like" (hint: check the bottom of this article 😜).

I wanted to make something like that for my Gatsby site so I can present readers with other articles they might potentially be interested in.

Who is this article for?

This is a post for people who already know the basics of how to build sites with Gatsby.js + Netlify. If you're not familar with it yet, definitely check out Gatsby and their excellent tutorials. If you'd like ME PERSONALLY to educate you on the topic, make sure to nag me and I'll see what I can do.

How we're going to do it

Since this is a site about Design Patterns and Principles, I'm going to show you one way to build something like this using the very fancy Builder Pattern in a way that doesn't feel fancy but feels powerful.

High level explanation

On my site, I organize all of my blog posts as Articles .

Each Article has one Category and can have many Tags .

If you haven't already figured out how to set up tags and category s, check out this cool guide on how to do that.

What I want to do is look at the Categories and Tags of all of my other articles and compute a similarity score with the current article to determine which articles are most similar to it.

Get Articles From a Graphql query

I've put together a SimilarArticles.js file that exposes a query (1.) of the same name.

The query returns every single article on my site, pulling in all the attributes I need from the article (or as I've included in my template-url, blog-post ) markdown files to render an article, including the Category and Tags .

import React from 'react' import PropTypes from 'prop-types' import { StaticQuery , graphql } from "gatsby" import { getPostsFromQuery } from '../../../../utils/blog' import ArticleCard from './ArticleCard' import { SimilarArticlesFactory } from './SimilarArticlesFactory' import "../styles/SimilarArticles.sass" const SimilarArticlesComponent = ( { articles } ) => ( < section className = "similar-articles" > { articles . map ( ( article , i ) => ( < ArticleCard { ... article . article } key = { i } / > ) ) } < / section > ) export default ( props ) => ( < StaticQuery query = { graphql ` query SimilarArticles { posts: allMarkdownRemark( sort: { order: DESC, fields: [frontmatter___date] } filter: { frontmatter: { templateKey: { eq: "blog-post" } published: { eq: true } } } limit: 1000 ) { edges { node { fields { slug readingTime { text } } frontmatter { title date description tags category image } } } } } ` } render = { data => { const { category , tags , currentArticleSlug } = props ; const articles = getPostsFromQuery ( data . posts ) ; const similarArticles = new SimilarArticlesFactory ( articles , currentArticleSlug ) . setMaxArticles ( 4 ) . setCategory ( category ) . setTags ( tags ) . getArticles ( ) return ( < SimilarArticlesComponent articles = { similarArticles } / > ) } } / > )

After I get all of my queries, I marshall them all (2.) into actual articles.

The Graphql responses are a little bit nested. Since I query articles often, I wrote a utility function to strip articles from a query.

Rank articles with the SimilarArticlesFactory

At (3.), things get fun.

I pass in all of the articles in addition to the currentArticleSlug of THIS article.

From what gets returned, I'm able to call setMaxArticles(num: number) , setCategories(category: string) , and setTags(tags: string[]) before calling getArticles() .

This is what's known as the builder pattern.

It works by returning this after calling the setter.

It's a Creational pattern that allows you to create objects in a more declarative way.

Let's look at the SimilarArticlesFactory.js .

import { includes , orderBy } from 'lodash' export class SimilarArticlesFactory { constructor ( articles , currentArticleSlug ) { this . articles = articles . filter ( ( aArticle ) => aArticle . slug !== currentArticleSlug ) ; this . currentArticleSlug = currentArticleSlug ; this . maxArticles = 3 ; this . category = null ; this . tags = [ ] } setMaxArticles ( m ) { this . maxArticles = m ; return this ; } setCategory ( aCategory ) { this . category = aCategory ; return this ; } setTags ( tagsArray ) { this . tags = tagsArray ; return this ; } getArticles ( ) { const { category , tags , articles , maxArticles } = this ; const identityMap = { } ; if ( ! ! tags === false || tags . length === 0 ) { console . error ( 'SimilarArticlesFactory: Tags not provided, use setTags().' ) return [ ] ; } if ( ! ! category === false ) { console . error ( 'SimilarArticlesFactory: Category not provided, use setCategory().' ) return [ ] ; } function getSlug ( article ) { return article . slug ; } function addToMap ( article ) { const slug = getSlug ( article ) ; if ( ! identityMap . hasOwnProperty ( slug ) ) { identityMap [ slug ] = { article : article , points : 0 } } } function addCategoryPoints ( article , category ) { const categoryPoints = 2 ; const slug = getSlug ( article ) ; if ( article . category === category ) { identityMap [ slug ] . points += categoryPoints ; } } function addTagsPoints ( article , tags ) { const tagPoint = 1 ; const slug = getSlug ( article ) ; article . tags . forEach ( ( aTag ) => { if ( includes ( tags , aTag ) ) { identityMap [ slug ] . points += tagPoint ; } } ) } function getIdentityMapAsArray ( ) { return Object . keys ( identityMap ) . map ( ( slug ) => identityMap [ slug ] ) ; } for ( let article of articles ) { addToMap ( article ) ; addCategoryPoints ( article , category ) ; addTagsPoints ( article , tags ) } const arrayIdentityMap = getIdentityMapAsArray ( ) ; const similarArticles = orderBy ( arrayIdentityMap , [ 'points' ] , [ 'desc' ] ) return similarArticles . splice ( 0 , maxArticles ) ; } }

The most interesting parts of this class are the setters, that return this , which allows that nice method chaining we saw earlier, and the getArticles() method.

We mentioned that we wanted to score articles based on similarity, right?

Well, this is one way we can do it.

I map over all of the articles (6.) and add them to an Identity Map (which is a fancy term for a hash table or a JavaScript object).

The way we identify two articles is by their slug .

While I'm doing that, I also look at the category for each article (7.).

If the current article has the category in common with this article that we're looping over, then we give it 2 points.

We do the same thing for tags (8.) but instead, we give the article 1 point for each tag that it has that's also in the current article.

At the end, we turn the entire thing into an array (9.) and then sort all of the articles (10. using lodash) from most to least points before slicing off the maxArticles requested, which was 4.

That's pretty much it! That's how you can build your own Similar Articles/Posts component with Gatsby.js.

It's a pretty common thing to see on blogs but I haven't seen it much in other Gatsby.js sites, because it doesn't come right out of the box.

Need more?

Scroll down a little bit to see it in action!

Need even more?

You can view all of the source code for this site on GitHub, it's fully open-sourced.