Skip to content

Creating a sidebar menu for accessing markdown headers as sections #853

@danielo515

Description

@danielo515

Hello,

This is some kind of mixture between issue and small how to tutorial.
I'm on a situation where I want to use markdown to generate a page, and I want each header on the page to have an anchor, so navigation between sections is possible.
This is very easy and straightforward just by using a markdown-it plugin. For example, adding something like this on the markdown-loader index.js file:

var md = markdownIt({
  html: true,
  linkify: true,
  typographer: true,
  highlight,
})
  .use(require('markdown-it-sub'))
  .use(require('markdown-it-footnote'))
  .use(require('markdown-it-deflist'))
  .use(require('markdown-it-abbr'))
  .use(require('markdown-it-attrs'))
  .use(require('markdown-it-anchor') // I was just one plugin away from happiness!!

As I said, easy setup, thanks Gatsby!
Let's go one step further: now I want a sidebar component that lists all the sections and allows me to navigate directly to them. But how ?
The options are:

  • Hardcode the sections on the markdown. I'm not a fan of hardcoding anything, so no no no
  • Use some dom traverse to find the headers, get their ID and build the sidebar. Not as bad as first option, but I don't like dom traverse because it's sloooow, and I'm using react to get away from it
  • Get the sections at compile time, and make them part of the page metadata. THIS is the approach I want. Sections are not going to change between compilations, and the result will be a dumb and performant component that just receives the list of headers and displays them as links.

Now that I'm sure that I want the third option, let's explore how can I accomplish this.
Luckily the developer of markdown-it-anchor is a good guy (many thanks @valeriangalliat, this would not be possible without you) and he provides you a hook (AKA callback) that is fired on every anchor generation. Can we use this callback to generate a list of sections ? The answer is yes and no. No with the current approach, yes if we made some changes.

So, the first problem is SCOPE. On the markdown-loader plugin (is this a plugin?) the markdown configuration is shared across all markdown files. This is a good performance practice, but this time is a problem for us.
What does this mean? If you just add the plugin to the list of markdown plugins like I showed above, and add a callback function to collect the headers links, something like this:

const sections = [ ];
var md = markdownIt({
 // bla bla bla options
})
 // Bla bla bla plugins
  .use( require('markdown-it-anchor', { callback: (token,info) => sections.push(info)}) 

then you will end with a beautiful array containing ALL the sections from all the markdown files, and each markdown file will receive it's own sections and the sections of all the files processed before.

Scope to the rescue!!! Moving the sections array, and the md plugin attachment declaration to the exported function will provide you that desired privacy. So, again, in the markdown-loader index.js file, the exported function looks like this:

module.exports = function (content) {
  this.cacheable()
  const sections = [];
  md = md.use( require('markdown-it-anchor'), { callback: (token, info) => sections.push(info)})
  const meta = frontMatter(content)
  const body = md.render(meta.body)
  const result = objectAssign({}, meta.attributes, {
    body, sections
  })
  this.value = result
  return `module.exports = ${JSON.stringify(result)}`
}

You see it? now sections is part of each file metadata just like fromMatter fields are. Very cool, and useful.

And here is the question part:
I don't know markdown-it architecture very well, but seems there is NO WAY of asking for some markdown metadata. Does anyone know a better approach ? I'm not very concerned about performance because this only happens at compile time and we are used to it being slow. But, if there is any better approach, in any sense, I would LOVE to know about it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions