Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
danielo515 opened this issue Apr 25, 2017 · 4 comments
Closed
Labels
type: question or discussion Issue discussing or asking a question about Gatsby

Comments

@danielo515
Copy link

danielo515 commented Apr 25, 2017

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.

@KyleAMathews
Copy link
Contributor

So sweet! It looks like you figured it out in a nicely elegant way.

What sort of metadata are you looking for?

@danielo515
Copy link
Author

To be honest, the only metadata I'm interested right now is the one that I wrote about.
But as usual, when I find I workaround for something I think that there should be a better way. That's why I have exposed my method here to help others and to allow others to help me 😄

@KyleAMathews
Copy link
Contributor

Fun :-)

So in 1.0 I've been using Remark instead of Markdown.js as it generates an AST that you can use to more easily pull out metadata.

So for your headers example, this is the code in Gatsby v1 to extract headers using Remark's AST

async function getHeadings(markdownNode) {

@jbolda jbolda added type: question or discussion Issue discussing or asking a question about Gatsby v0 labels Jun 5, 2017
@KyleAMathews
Copy link
Contributor

Closed as not support v0 anymore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question or discussion Issue discussing or asking a question about Gatsby
3 participants