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.
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
and change into the work directory:
$ cd jamify-toc
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
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`,
},
]
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
And visit your site at http://localhost:8000
as usual. Switch to an article containing headlines in order to see the ToC in action:
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:
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,
}
},
]
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 /
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
}
}
}
}
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
Every plugin must contain a package.json
file. We can create it with npm init
:
[gatsby-rehype-chapters]$ npm init
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"
}
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
}
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`,
},
],
},
},
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:
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`)};
}
}
`
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/
Take note on how the directory structure matches the original:
src/gatsby-theme-ghost-toc/
└── components
└── common
└── TableOfContentStyles.js
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
After rebuilding the project with yarn develop
, you should see the color change in the ToC highlight color.
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!
This post was originally published at jamify.org on April 21, 2020.
Top comments (0)