Related articles with Gatsby
Posted 20.02.2019 · 5 min readThis website is created with Gatsby, Reasonml and a little bit of typescript. In this post I will explain how I built the "Related articles" feature that can be seen on the bottom of some of the articles. I have kept all the examples in this post in javascript so it will be helpful to more people.
First of we need to define what makes two posts or articles related. On this site and for this article I am defining it as articles that has the same tags. In the frontmatter I have a comma separated list of tags. Below is an example:
title: Related articles with Gatsbyslug: related-articles-with-gatsbytags: gatsby,javascriptdate: 2019-02-20
This blog is based on a setup with gatsby-transformer-remark which has the
following in gatsby-node.js
to generate the pages. The first part fetches all
the articles and the necessary metadata with Graphql. The second part loops over
all the articles and then creates pages for them with the Gatsby createPage
function.
exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions; const templatePath = path.resolve('./src/templates/');
const response = await graphql( ` query { allMarkdownRemark { edges { node { frontmatter { layout slug date(formatString: "DD.MM.YYYY") } } } } } ` );
const postEdges = response.data.allMarkdownRemark.edges
postEdges.forEach(({ node }) => { const { slug, layout, tags } = node.frontmatter; createPage({ path: `/${slug}`, component: path.join(templatePath, `${layout}.tsx`), context: { slug }, }); });
In order to populate the data for related articles we will add the data as context parameters in the createPage call. However, first we need to find those articles. The code below will build a map of tags and articles with those tags.
const articlesByTags = postEdges.reduce((lastValue, { node }) => { const tags = node.frontmatter.tags || ""; tags.split(/, ?/).forEach(tag => { if (lastValue[tag]) { lastValue[tag].push(node.frontmatter); } else { lastValue[tag] = [node.frontmatter]; } }); return lastValue;}, {});
The result will be something like the following, where {...}
is the
frontmatter of a post. This we can use to look up all the related articles for
one post.
{ gatsby: [{...}, {...}], cli: [{...}, {...}]}
The articlesByTags
is useful for looking up all the articles that has a list of tags. Let
us look at a possible implementation for that.
const _ = require("lodash/fp");
function findRelatedArticles(slug, tags) { return _.flow([ _.split(/, ?/), _.map(tag => articlesByTags[tag]), _.flatten, _.filter(node => node.slug !== slug), _.uniqBy(node => node.slug) ])(tags);}
In this function the _
is the
functional programming(fp) version of lodash, which has this neat
utility called flow for grouping actions together. Flow returns a function when
called will perform all the operations in the array. Each operation will have
to be a function and the return value of each of them will be passed as the
argument to the next one. The function above will first split the comma
separated string to a list and then perform a map on that list of tags to a
list of lists of articles. Since we now have a list of lists it is necessary to
flatten it, which will result in a list of articles. The next step is to filter
out the current post since we only want other articles than the one that is
currently viewed. In the last step we want to remove any duplicate articles and
can do that by selecting unique elements based on the slug.
To finish the data setup it is necessary to add the related articles to the
context in the createPage
call.
createPage({ path: `/${slug}`, component: path.join(templatePath, `${layout}.tsx`), context: { slug, relatedArticles: findRelatedArticles(slug, tags), },});
The gatsby-node.js
file should look something like the following now. In the
code below the changes is highlighted.
exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions; const templatePath = path.resolve('./src/templates/');
const response = await graphql( ` query { allMarkdownRemark { edges { node { frontmatter { layout slug date(formatString: "DD.MM.YYYY") } } } } } ` );
const postEdges = response.data.allMarkdownRemark.edges;
const articlesByTags = postEdges.reduce((lastValue, { node }) => { const tags = node.frontmatter.tags || ''; tags.split(/, ?/).forEach(tag => { if (lastValue[tag]) { lastValue[tag].push(node.frontmatter); } else { lastValue[tag] = [node.frontmatter]; } }); return lastValue; }, {});
function findRelatedArticles(slug, tags) { return _.flow([ _.split(/, ?/), _.map(tag => articlesByTags[tag]), _.flatten, _.filter(node => node.slug !== slug), _.uniqBy(node => node.slug), ])(tags); }
postEdges.forEach(({ node }) => { const { slug, layout, tags } = node.frontmatter; createPage({ path: `/${slug}`, component: path.join(templatePath, `${layout}.tsx`), context: { slug, relatedArticles: findRelatedArticles(slug, tags), }, }); });
At this point all the data needed is in place. However, it still has to be shown to give some value. Let us look at a component to add this to the site.
export function RelatedArticles(props) { if (!props.articles || props.articles.length === 0) { return <div />; } return ( <section id="related-articles"> <header>Related articles</header> <ul> {props.articles.map(article => ( <li key={article.slug}> <Link to={`/${article.slug}`}>{article.title}</Link> {' '} <em>{article.date}</em> </li> ))} </ul> </section> );}
This component will render an empty div if there are no related articles.
In the case of related articles it will render a list with links to the
articles. Remember that the data available here is defined by the graphql
query in gatbsy-node.js
. In order to glue these parts together and make
it work the component needs to be added in the template used for the
articles.
<RelatedArticles articles={props.pageContext.relatedArticles} />
The page component will get the context
attribute from createPage
as
pageContext
. Thus, the relatedArticles
attribute we created earlier will
be accessible on that object.
This is all that is needed to have related articles with a Gatsby site with the transformer-remark plugin. To see have it looks have a look at the bottom of the Testing simple GraphQL services article.