Article recommendation using Hugo
Tags: hugo, writting, devtips
Published: 2018-10-09 12:59:55

---
title: Article recommendation using Hugo
published: true
tags: hugo,writting,devtips
cover_image: https://thepracticaldev.s3.amazonaws.com/i/o6bax2pwxxzfhv1ko5sl.png
description: How to generate a list of recommended articles for your site using Hugo
canonical_url: https://ops.tips/blog/article-recommendation-using-hugo/
---

Hey,

This month I’ve been interested in increasing the overall time that a user spends navigating there in the blog ([ops.tips](https://ops.tips)).

Being article recommendation something that the blog was missing, I went for it.

![Article recommendation in use](https://ops.tips/blog/-/images/recommended-articles.svg)

Check out how you can do it for your static site too!


### The idea

Given that my content usually is tagged, I thought that one easy way of adding article recommendation would be to simply take the union between the articles tagged with the same tag as the current one and then randomly list them.

For instance, consider the case of this article: [A UDP server and client in Go](https://ops.tips/blog/udp-client-and-server-in-go/).

Being the article tagged as [Linux](https://ops.tips/tags/linux), [Networking](https://ops.tips/tags/networking), and [Go](https://ops.tips/tags/go), we can infer that there are three pools of content that might be similar to what’s been there in this article (having some overlap between them).

![Overlap between bag of articles by selecting articles with tags matching](https://ops.tips/blog/-/images/recommendation-overlap.svg)

Moving forward, we can think that once we addressed the possible articles to recommend, the next step is to give preference to some of them: rank higher those with more overlap in more categories.

Let’s implement that then.


### Implementing a Recommended Articles list in Hugo

As a first step, we retrive all the pages that are not the currently page that we are seeing:

```scala
// Assign the current scope (the current page) to
// a variable named `$currentArticle` so that within
// other scopes we are able to still reference the 
// current page.
{{ $currentArticle := . }}

// From the list of all pages from the site, only keep
// those whose name is different from the name of the
// current article and whose kind is `page`.
//
// ps.: here we could check something else, like permalink
// or another unique identifier.
// 
// ps.: here you'd probably pick a specific section. To do
// so, perform another `where`.
{{ $articles := where 
        (where $.Site.Pages ".Kind" "eq" "page") 
        ".Title" "!=" $currentArticle.Title }}
```

note.: the `where` above **must** be inlined. Here I broke it in multiple lines just for achieving better readability.

Next, we now create two lists:

1. one that references all articles with **at least two** tags that are in the set of tags in the current article; and
2. a list that references all articles with **a single** tag in the set of tags of the current article.

We can call these two `veryRelevantArticles` and `relevantArticles` (accordingly):

```scala
// Instantiate each of them with an empty slice.
{{ $veryRelevantArticles := slice }}
{{ $relevantArticles := slice }}
```

With the variables set, we can now start iterating over our list of all article pages and checking how many intersections they have with the set of tags from our current page:

```scala
// Iterate over each of the articles from the list 
// of article pages
{{ range $idx, $article := $articles }}
        // Compute the number of tag intersactions.
        {{ $numberOfIntersections := len (
                intersect $article.Params.tags $currentArticle.Params.Tags
        ) }}

        // For those pages with a big number of 
        // intersections (>= 2), put in the first
        // slice.
        {{ if (ge $numberOfIntersections 2) }}
                {{ $veryRelevantArticles = 
                        $veryRelevantArticles | append $article }}
        // For the rest (single intersaction), put in the 
        // second slice.
        {{ else if (eq $numberOfIntersections 1) }}
                {{ $relevantArticles = 
                        $relevantArticles | append $article }}
        {{ end }}

        // note.: I'm ignoring those with 0 intersections.
{{ end }}
```

Once we’ve gotten all interesting articles, now we can create an ordered list starting from those with the biggest number of recommendations to those with the lowest, i.e., we can create a list that corresponds to the concatenation of the two variables we created above:

```scala
// Create an empty slice to hold the final list
{{ $recommendedArticles := slice }}

// For each very recommended article, append to the
// list.
{{ range $veryRelevantArticles }} 
        {{ $recommendedArticles = $recommendedArticles | append . }} 
{{ end }}

// For each recommended article, append to the
// list.
// 
// This will lead to something like 
// [very, very, rec, rec, rec....]
{{ range $relevantArticles }} 
        {{ $recommendedArticles = $recommendedArticles | append . }} 
{{ end }}
```

With the list created, now it’s time to show it.


### Displaying the recommendation list

With a list containing those with the biggest number of intersections first and then the ones that have the less number of intersections, we can move forward with displaying that.

For this Blog, I took the approach of showing a shuffle of the very first five that are picked from such list.

```scala
<ul>

// For every article in the set of the first 5
// recommended articles shuffled, show their
// anchor.
{{ range (shuffle (first 5 $recommendedArticles)) }}
<li>
  <a href="{{ .Permalink }}">
    {{ .Title }}
  </a>
</li>
{{ end }}

</ul>
```

This guarantees that if I have some articles with big intersections with the content of the current article, they get displayed (even though unordered).

If you have a lot of content, and a lot of tags, you might want to create more categories (not only `veryRelevant` and `relevant`).

In such case, something more elaborate could be done.


### Closing thoughts

It’s interesting to see how much we can accomplish without “an actual language”.

Although Hugo gives us some simple primitives, we can build upon that and achieve some pretty satisfactory results.

What about you? Have you ever achieved something similar doing something different? Please let me know!

Also, if you have any questions, feel free to drop me a message at [@cirowrc](https://twitter.com/cirowrc) on Twitter.

Have a good one!