Enhance Your Prismic + Gatsby Site with a Table of Contents

The argument for placing a table of contents in articles and blog posts becomes clear when:

  • The author wants logical sectioning in their writing as a navigational roadmap
  • The writing is extensive and references required to locate important pieces of information
  • The content follows a repeatable pattern which is comparable side-by-side

The Gatsby+Prismic combination

Gatsby is a free and open-source framework based on React that helps developers build blazing-fast websites and apps. Data is fetched from a comprehensive set of sources at build time and compiled to static HTML pages. The pages are then served to the client in a performant manner.

In this case, one of our many potential data sources is Prismic.

Prismic is a headless CMS that integrates with Gatsby via dedicated plugins. It has a super-customizable content builder and a powerful API.

I could not find a built-in table of contents component on Prismic’s dashboard, so I set out to remedy that. I am now sharing what I was able to achieve.

Configuring Prismic

Configuring a content blueprint

There are endless ways to configure Prismic according to your needs. Logic dictates that there should exist a template on which to model content. The configuration of this building block will be a custom type in Prismic.

Custom types allow you to define and configure fields for your content. Some examples of custom types are pages, posts, articles, authors, and products. These are blocks of content you need for your specific project.

If you are starting with development from scratch, it would be beneficial to configure a custom type for your content so we can work on it.

Creating the table of contents slice

Prismic has an option to customize custom types by adding ‘dynamic zones’ called slices. Slices allow ultimate customizability by giving users the ability to add zones anywhere in their content. These can be a text section, an image, or a quote among many others. You can add your slice, which to me seemed to be the logical basis for our table of contents component.

Let’s create a slice named ‘Table of Contents’ and configure it to contain two zones. We want the optional table title to be in a non-repeatable zone and the title and anchor links in a repeatable zone. The elements in the repeatable zone will form the navigational links in the table. Keeping section_title and section_anchor separate allows complete customizability. We configure different titles for article headings to keep things concise. Accessibility and SEO friendly anchor links will keep Lighthouse happy. Here is what this configuration looks like:

You can pick and choose any of the 10+ elements in Prismic’s dashboard. A link to external media? A content relationship? Some images? Let’s keep additions to a minimum for the sake of simplicity.

Adding click-based navigation support

Now, we need to configure (optional) anchors in our repeatable Text slice. In this case, this slice is the logical section that creates the body of our article. You may have configured Prismic differently depending on your needs, but the rationale is the same. For click-based navigation to work, we need two elements:

  1. The anchor at the start of Text slices
  2. The anchors in the table

Every corresponding pair needs to be identical and follow HTML anchor rules.

Next up, we configure our Gatsby frontend to consume our table of contents slice.

Configuring Gatsby and React

Adding a basic React component

If you do not have a project set up, gatsby-universal is a decent (and opinionated) starter project which will serve our purpose fine.

Let us start by adding a basic React component which will render our Table of Contents. We pass title and items to it as props, matching the repeatable and non-repeatable zones designed earlier. Starting with a wrapper div, we add an h2 for the title and map over items to render out anchors. We use index to number the items in the list.

import React from 'react'
import styled from 'styled-components'

const Wrap = styled``
const Title = styled``
const Anchor = styled``
const Index = styled``

export const TableOfContents = (props) => {
  const { title, items } = props
  return (
    <Wrap>
      <Title>{title}</Title>
      {items.map((item, i) => (
        <Anchor
          key={item.section_anchor.text}
          href={`#${item.section_anchor.text}`}
        >
          <Index>{i + 1}</Index>
          <p>{item.section_title.text}</p>
        </Anchor>
      ))}
    </Wrap>
  )
}

Configuring the article template

There are a couple of plugins that help to integrate Prismic with Gatsby, each with their particular setup process. I will be using the version 2 of gatsby-source-prismic in this guide.

Gatsby allows page creation from external data as explained here. The essence of this process is:

  1. Querying data with GraphQL during build time
  2. Using the createPages API to map query results to pages using a template

Each page is provided the uid of an article via context. This will allow Gatsby to fetch each article in the template article.js while building static HTML. Standard Gatsby magic outlined here, nothing exceptional.

The GraphQL query that fetches an article can be viewed and debugged on Gatsby’s GraphiQL environment.

The secret sauce is GraphQL fragments that relate to Prismic slices and which will form the body of our article. These must be fetched from Prismic via their API and the way to do that is to include them in the template query.

We need to add the GraphQL fragment that relates to the table of contents. The fragment name is available on Gatsby’s dev server which runs by default at localhost:8000/\_\_\_graphql.

Rendering the article

Article data is now available to our Article template component as a prop. How you want to handle rendering the article is up to you; the important step of making the required data available is already completed.

That’s it! We added a table of contents component to a Gatsby site powered by Prismic without too much trouble, and with a lot of customizability. Win-win!