Astro is a static site generator that allows you to write your content in Markdown. When you write a long blog post, you might want to add a table of contents to help your readers navigate the content.
There is already a plugin that generates a table of contents within the Markdown file. But what if you want to show the table of contents somewhere else on the page, like in the sidebar on this page?
In this article, we cover how to write a plugin that extracts headings from a Markdown file and adds a table of content array to the frontmatter
of Astro. Later, we use it to generate the table of contents.
Let’s say we write a blog article in a Markdown file as above. We have the title and date of publication in the frontmatter
and the content in the body of the Markdown file.
We want to display the table of contents somewhere on the page that that contains the text value of the headnigs:
- Writing a Remark Plugin
- Creating a Table of Contents
Writing a Remark
Plugin
We write a Remark plugin that extracts the headings of the Markdown file and adds them to the frontmatter
of Astro. We use the unist-util-visit
package to traverse the AST of the Markdown file.
What is Remark?
Remark is a Markdown processor powered by plugins. With Remark, you can traverse and transform your Markdown content. Astro comes with built-in support for Remark.First, install the unist-util-visit
package:
Then, create a file table-of-content-plugin.js
with the following content.
This plugin uses the visit
function to find all the heading
nodes in the AST. We only consider top-level headings that are direct children of the root
node. Then, we collect the heading’s title
, depth
, and href
into an array.
We use the getNodeValue
utility function that recursively traverses the children of the heading and concatenates their text content. This is important if a heading contains inline elements like an inline code block.
The href
is generated from the title by converting it to lowercase, removing special characters, and replacing spaces with dashes. This is the same method as Astro generates the id
attribute for headings.
Finally, once we traversed every heading, we assign the result to the tableOfContents
field of the Astro’s frontmatter
. When we define it in Markdown, the frontmatter
section has to be in the YAML format. However, when we set it in the plugin, we can simply assign a JavaScript value.
Configuring Astro
Now that we have our plugin, we can extend our Astro config to make use of it. This will automatically process every markdown file and add the tableOfContents
field to the frontmatter
.
Table of Contents component
Now, that we have the table of contents in the frontmatter
, we can use it to render the table of contents in our Astro components.
Let’s say we have a src/components/blog/BlogContent.astro
component that renders the content of a blog post. We can access the tableOfContents
field from the frontmatter
and pass it on to the TableOfContents
component.
Accessing fields added by plugins is different. Unlike accessing the original frontmatter
data, like the title
field, we need to render
the entry first to get the processed content.
The render function returns the remarkPluginFrontmatter
object that contains the fields added by our remark plugin.
In the TableOfContents.astro
component, we can render the list of headings as an ordered list. We use the depth
field to determine the nesting level of the list items.
The table of contents on this page was created in a similar way. There’s one difference. It also includes an Introduction link that jumps to the beginning of the article. We can easily add this by setting an id
to the main
element above and adding a static list element to the TableOfContents
component.