The Daily TIL

March 5, 2019 by AndyAndygatsbybloggingjavascript

Adding tags to your Gatsby blog entries

This blog now has tags. It was super easy to add these, and I didn’t need any additional NPM packages or 3rd party services.

TL;DR; You can pre-generate all your tag-index pages at build-time using Gatsby’s createPages API and some simple javascript.

Example

Assuming you have a tags array field in your headless CMS’s data-model (what, you don’t use a headless CMS?? Here are a ton of options), you can create groupings of all of your blog entries by tag using this simple example in your gatsby-node.js. It’s basically the same as the gatsby-starter-blog example, but with the highlighted additions for tag-indexing.

full example here

Creating the entry and tag pages

// Helper to build the tag index
const indexEntry = (index, tag, entry) => {
  if (!index[tag]) {
    index[tag] = [];
  }

  index[tag].push(entry);
};

exports.createPages = ({ graphql, actions: { createPage } }) => {
  const tagIndexTemplate = path.resolve('./src/templates/tagIndex.js');

  // Find all of my blog entries with tags
  return graphql(`
      {
        allContentfulEntries {
          edges {
            node {
              slug // the entry slug
              tags // the tags array
            }          }
        }
      }
  `).then(result => {
    const entries = result.data.allContentfulEntries.edges;
    const tagIndex = {};
    
    // Iterate over all entries, building the tag index as we create the entry pages
    entries.forEach((entry, index) => {
      // Create the actual entry page
      createPage({
        path: `/entry/${entry.node.slug}`,
        component: entryTemplate,
        context: {
          slug: entry.node.slug,
        },
      });
      
      // Add this page to the tag index      entry.node.tags.forEach(tag => {        indexEntry(tagIndex, tag, entry.node);      });    });

    // Build the tag pages from the tag index
    Object.keys(tagIndex).forEach(tag => {
      createPage({        path: `/tag/${tag}`,        component: tagIndexTemplate,        // Pass the tag and the list of slugs in the index to the page template        // The page template will use these to render the list of entries for the tag        context: {          tag: tag,          pages: tagIndex[tag],        },      });    });
  });
};

According to Gatsby createPage docs,

Context data for this page. Passed as props to the component this.props.pageContext as well as to the graphql query as graphql arguments.

So now that we’ve passed pages as context to the tagIndex page template, we can do this…

Rendering a tag page

const TagIndexPage = ({
  // passed in from createPage
  pageContext: { tag, pages },
  // passed in from pageQuery results
  data: {
    allContentfulTil: { edges },
  },
}) => {
  // Helper which turns the entry edge array into a map of slug -> entry
  // We'll use this below to look up all the metadata for the entry
  const allTils = flattenTils(edges);

  return (
    <Layout>
      <header><h2>{tag}</h2></header>
      {pages.map(({ slug }) => 
        <Link to={`/entry/${slug}`}>{allTils[slug].title}</a>
      )}
    </Layout>
  );
};

flattenTils helper

Here’s the helper used to convert an array of edges returned from the Gatsby pagequery to a map. I use this technique quite a bit to create lookups for Gatsby data arrays.

import { mergeAll, map, prop, compose } from 'ramda';

// [{ node: { slug: "entry-1", title: "entry one", ... } }] => { "entry-1": { title: "entry one", ... } }
export default compose(
  // [{ slug1: entry1 }, { slug2: entry2 }] => { slug1: entry1, slug2: entry2 }
  mergeAll,
  // [entry] => [{ slug: entry }]
  map(node => ({
    [node.slug]: node,
  })),
  // [{ node: { ... } }, { node: { ... } }] => [{ ... }. { ... }]
  map(prop('node'))
);

References