DEV Community

Joost Jansky
Joost Jansky

Posted on • Originally published at jamify.org on

Adding a table of contents

Learn how you can add an auto-generated, interactive Tables of Contents pane within seconds. That's why I have plenty of time to delve into more advanced customization techniques. Learn how to write your own plugin for chapter numberings and make use of component shadowing to customize styles.

Adding a table of contents

A table of contents section, commonly abbreviated as ToC, gives your readers a quick overview of the topics you touch upon and makes it easier to browse through your article.

In electronic publications, a ToC can be made interactive, so chapters of interest are just one click away. While a random blog article may not need a ToC, tutorials, guides and documentation articles of all kinds, benefit a lot from it.

Jamify starter

Let's start jamming by downloading a fresh starter project from Github:

$ git clone https://github.com/styxlab/gatsby-starter-try-ghost.git jamify-toc
Enter fullscreen mode Exit fullscreen mode

and change into the work directory:

$ cd jamify-toc
Enter fullscreen mode Exit fullscreen mode

Installing the ToC

Here you can fully leverage Jamify's plugin approach. As is the case for many Gatsby projects, new functionality can be integrated by installing a plugin thereby harnessing the power of the javascript plugin eco-system. Install the following plugin to integrate a ToC box into your site's user interface:

$ yarn add gatsby-theme-ghost-toc
Enter fullscreen mode Exit fullscreen mode

The plugin must be registered in gatsby-config.js, put it right after the gatsby-transformer-rehype plugin:

// In your gatsby-config.js
plugins: [
    {
        resolve: `gatsby-theme-ghost-toc`,
    },
]
Enter fullscreen mode Exit fullscreen mode

Note that the ToC is auto-generated from the headlines of each article. This is done by the plugin gatsby-transformer-rehype which is already included in the starter. That's why you do not need to install this dependency.

Fire up the development build:

[jamify-toc]$ yarn develop
Enter fullscreen mode Exit fullscreen mode

And visit your site at http://localhost:8000 as usual. Switch to an article containing headlines in order to see the ToC in action:

Adding a table of contents

Adding a table of contents

For small screens, a ToC is added after the feature image and right before the main content area. For larger screens, the ToC is moved to the right hand side and is made sticky, so it is always visible when you scroll down.

The current document position is highlighted in color, giving your readers a visual indication of the current location within the document. Clicking on ToC items brings you directly to the chapter. If you have wide content such as full screen gallery pictures, the ToC box slides under your images:

Adding a table of contents

Depending on the headline level, ToC items are indented. You can see this in the picture above, where the h3 heading Image sizes is moved to the right, visualizing the relationship to its h2 parent Working with images in posts.

Configuration Options

If you are missing a configuration option in the gatsby-theme-ghost-toc plugin, tell us and we might implement it in a future release. For the time being, there is just one option available:

// In your gatsby-config.js
plugins: [
    {
        resolve: `gatsby-theme-ghost-toc`,
        options: {
            maxDepth: 2,
        }
    },
]
Enter fullscreen mode Exit fullscreen mode

The maxDepth option restricts the number of shown headline levels. With the default value of 2 you get a primary heading and one sub-heading. You can increase this number up to 6 levels, but if your heading levels get too convoluted it doesn't look great on your page. A value between one and three should be fine for most use cases.

Developer friendliness

From a user's perspective, this is all you need to know and the given options may be exactly what you have been looking for.

However, you may have different needs. If you are designing a blog for a customer, you may want to know how you can further customize the style of the ToC. As an application developer, you may wish to understand how the automatic ToC generation is working.

Jamify is all about enabling you to achieve your publishing goals, giving you a framework that is easy to extend and fun to work with.

Let's delve into these more advanced topics that are connected to the underlying framework before making specific customizations to the ToC.

Automatic ToC Generation

I mentioned earlier that the ToC tree structure is generated in the transformer plugin that comes pre-installed with the starter. So, what is the gatsby-transformer-rehype plugin actually doing?

It's main task is to take an html fragment as input, make some transformations to it and spit out the changed html again. The special property of the transformation is that it uses an intermediate transformation step:

HTML -> htmlAST -> htmlAST -> HTML
        └── plugin1: mutating htmlAST / └── -> ToC 
                              └── plugin2: mutating /    
Enter fullscreen mode Exit fullscreen mode

The HTML is never changed directly, it is first transformed to a so called syntax tree, here it is an html syntax tree called htmlAST. The cool thing about the syntax tree is that it's really easy to manipulate it and that you can use standard libraries to do so.

Another important concept is that the transformer plugin usually does not manipulate the htmlAST itself, it delegates this work to sub-plugins. All sub-plugins to gatsby-transformer-rehype follow a naming convention, so they start with gatsby-rehype-* which indicates that they perform a specific manipulation task on the htmlAST.

What does this have to do with the automatic ToC generation? Well, gatsby-transformer-rehype generates a ToC from the last htmlAST, after all sub-plugins have completed their transformations. This ToC is then put into the GraphQL schema and you can query it from your React components as usual:

{
  allHtmlRehype {
    edges {
      node {
        html
        tableOfContents
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is exactly how the gatsby-theme-ghost-toc plugin is retrieving the tableOfContents data, using it to make a great user interface out of it.

Armed with this information about the Jamify framework, you can now make changes to the ToC data.

Chapter numbering

If you want to publish a thesis or a book, your chapters and subsections typically follow a numbering scheme. How can you automatically generate chapter numbers, add them to the headings and make sure they also show up in your ToC?

As there is currently no standard plugin available for this use case, you can easily write your own. Before doing so, let's think about how this requirement can be accomplished with Jamify. From what you have learned in the previous chapter, it's quite obvious that you can use a gatsby-rehype-* plugin: For every post, you get an htmlAst from the transformer that you can manipulate.

So, the idea is to create a plugin named gatsby-rehype-chapter, where you take the htmlAst, visit each heading and add the chapter number to it. As the mutated htmlAst ist automatically transformed back to html by the transformer, your changes will then show up both in your headings and in your ToC. The difficult part is to write a function that generates correct numberings, but that's not hard either.

Your first rehype plugin

When starting a new plugin it's always a good idea to implement and test it locally. Gatsby provides an extremely convenient way to do that. You simply place your plugin in a folder called plugins and it will be resolved like external plugins during build time:

[jamify-toc]$ mkdir -p plugins/gatsby-rehype-chapters
[jamify-toc]$ cd plugins/gatsby-rehype-chapters
Enter fullscreen mode Exit fullscreen mode

Every plugin must contain a package.json file. We can create it with npm init:

[gatsby-rehype-chapters]$ npm init
Enter fullscreen mode Exit fullscreen mode

Answer the upcoming questions as follows:

{
  "name": "gatsby-rehype-chapters",
  "version": "1.0.0",
  "description": "Add chapter numbering to headings",
  "main": "gatsby-node.js",
  "author": "",
  "license": "MIT"
}

Enter fullscreen mode Exit fullscreen mode

It's import that you type gatsby-node.js as your entry point instead of the default index.js. This is the file where we implement the plugin functionality. Create and open a new gatsby-node.js file in your favourite editor and paste the following code into it:

const visit = require(`unist-util-visit`)

module.exports = ({
  htmlAst,
  generateTableOfContents,
  htmlNode,
  getNode,
  reporter
}) => {

    const map = []

    // recursive walk to generate numbering
    const numberingOfChapters = (toc, numbering = ``, depth = 1, chapter = 1) => {
        toc.forEach((node) => {
            map.push({ id: node.id, numbering: `${numbering}${chapter}.` })
            if (node.items && node.items.length > 0 && depth < 7) {
                numberingOfChapters(node.items, `${numbering}${chapter}.`, depth + 1)
            }
            chapter = chapter + 1
        })
    }
    numberingOfChapters(generateTableOfContents(htmlAst))

    const tags = [`h1`,`h2`,`h3`,`h4`,`h5`,`h6`]
    const headings = node => tags.includes(node.tagName)

    visit(htmlAst, headings, node => {
        const id = node.properties && node.properties.id || `error-missing-id`
        const [child] = node.children
        if (child.type === `text`) {
            const { numbering } = map.find(node => node.id === id)
            child.value = numbering + ` ` + child.value
        }
    })

    return htmlAst
}

Enter fullscreen mode Exit fullscreen mode

This is a lot of code, but I walk you through it. You export one function, which needs to be called by the transformer plugin. That's why the function must conform to the transformer's requirements: It's got a specific list of input arguments (most notably htmlAst and some others) and it must return the changed htmlAst. All gatsby-rehype-* plugins are of this form, taking htmlAst as input, manipulate it and give it back again. It may look trivial at first, but it's a powerful concept.

The function body performs two tasks. The first part generates the numbering within function numberingOfChapters. The second part utilizes the visit function which is used here to find the headings within the htmlAst. Once a heading is found, it's previously generated numbering is looked up from the map and then prefixed to the heading.

The numberingOfChapters is quite interesting. It uses recursion for generating all sub-levels and it is called with a tree structure that was generated with the function generateTableOfContents(htmlAst). So, we can leverage the on-demand computed ToC structure to generate the numberings!

You may ask yourself how the ToC can contain the numbering, if the plugin only mutates the headings. This magic is possible due to a clever design of the transformer: a final ToC is always generated based on the latest mutation!

Register your plugin

There is only one final step missing. You need to register your plugin in your gatsby-config.js. Don't forget to change to the root of your working directory and make the following changes:

// gatsby-config.js
{
    resolve: `gatsby-transformer-rehype`,
    options: {
        filter: node => (
            node.internal.type === `GhostPost` ||
            node.internal.type === `GhostPage`
        ) && node.slug !== `data-schema`,
        plugins: [
            {
                resolve: `gatsby-rehype-ghost-links`,
            },
            {
                resolve: `gatsby-rehype-prismjs`,
            },
            {
                resolve: `gatsby-rehype-chapters`,
            },
        ],
    },
},
Enter fullscreen mode Exit fullscreen mode

The gatsby-rehype-chapters plugin must be registered as a sub-plugin of gatsby-transformer-rehype. As the other two gatsby-rehype-* plugins are doing completely different things, the order within the sub-plugins does not matter here.

Numbering in action

It's time to inspect the results. Rebuild the project in development mode with yarn develop and go to one of the posts:

Adding a table of contents

As you can see, numberings have been added to the titles and the Table of Contents side pane.

The data generation and user interface ToC pane is entirely done during build time, effectively offloading computational heavy work from the point in time where you site is served to your visitors. This is the crucial concept behind making your sites flaring fast.

Customize Styling

So far you have worked with making changes to the underlying data: the headings and the ToC that is generated from it. Now, it's time to make changes to the style. We could look at the code of plugin gatsby-theme-ghost-toc and make changes directly there.

However, if you only intend to make a small change, there is a much better way to customize gatsby-theme-ghost-toc: through component shadowing. The basic idea is that you only rewrite a small portion of the plugin that is relevant to you. You just need to put a matching file into a place where Gatsby can replace the original.

Let's use component shadowing to change the highlighting color of the ToC from green to blue. As a first step, you need to locate the code where the ToC is styled. The relevant code block can be found in fileTableOfContentStyles.js, at the end:

export const TocLink = styled(Link)`
    && {
        height: 100%;
        box-shadow: none;
        color: ${props => (props.state.isActive ? `#54BC4B` : `inherit`)} !important;
        border-bottom: ${props => (props.state.isActive ? `1px solid #54BC4B` : `none`)};
        text-decoration: none;
        &:hover {
            color: #54BC4B !important;
            border-bottom: 1px solid #54BC4B;
            text-decoration: none;
            box-shadow: none;
        }
        &::before {
            background-color: #EEE;
            content:' ';
            display: inline-block;
            height: inherit;
            left: 0;
            position:absolute;
            width: 2px;
            margin-left: 1px;
        }
        &::before {
            background-color: ${props => (props.state.isActive ? `#54BC4B` : `#EEE`)};
        }
    }
`
Enter fullscreen mode Exit fullscreen mode

There are five occurrences of the green color #54BC4B that we want to replace with the blue color #30B4F9 on a copy of that file. The copy must be put into a replicated folder structure under the src/ folder:

[jamify-toc]$ mkdir -p src/gatsby-theme-ghost-toc/components/common
[jamify-toc]$ cp node_modules/gatsby-theme-ghost-toc/components/common/TableOfContentStyles.js src/gatsby-theme-ghost-toc/components/common/
Enter fullscreen mode Exit fullscreen mode

Take note on how the directory structure matches the original:

src/gatsby-theme-ghost-toc/
└── components
    └── common
        └── TableOfContentStyles.js

Enter fullscreen mode Exit fullscreen mode

Finally you can replace the color with this one-liner:

[jamify-toc]$ sed -i "s|#54BC4B|#30B4F9|g" src/gatsby-theme-ghost-toc/components/common/TableOfContentStyles.js
Enter fullscreen mode Exit fullscreen mode

After rebuilding the project with yarn develop, you should see the color change in the ToC highlight color.

Adding a table of contents

Note that you only changed one file of the gatsby-theme-ghost-toc plugin, so your shadowed file will most likely work even after a new plugin version has been published. That's another benefit of component shadowing: you still get the improvements that are published to the original plugin.

Summary

I hope this tutorial conveyed that adding a ToC to you Jamify site is dead simple. This pre-configured ToC already contains a lot of cool features: auto-generation of ToC from existing headers, moving the ToC box to the side for large screens, jumping to a headline by clicking a ToC item, highlighting of ToC items as you scroll through the document and an configuration option to control the headline levels.

While this is great, you can do even more! You learned how to write a rehype plugin that let's you generate and add chapter numberings to you titles and ToC items. Once you come to the realization that you can use exactly the same approach to make changes to anything within your blog content, you'll start feeling the power in your own hands. Use it with care!

While the plugin approach is best suited for content changes or computations based on content, customizations to the presentation layer such as style changes are usually best accomplished with component shadowing. This is another super powerful concept that you learned in this tutorial. You used it to change the highlighting color of ToC items, but it can be used to change any functionality of every plugin that you use in your project. Even more power at your hands!

Do you want early access to Blogody, the brand new blogging platform that I am creating? Just sign-up on the new Blogody landing page and be among the first to get notified!


This post was originally published at jamify.org on April 21, 2020.

Top comments (0)