Table of Contents Component

Builds a table of contents from the markdown headers

astromarkdown
Published 2022-02-22
This is it right here!!
//TODO: This post needs a revision to a recent Astro version.

Introduction

This component will render a Table of Contents section following your markdown heading structure. Each item is linked to the respective header in the document, so you can click and jump to the section.

How it works

Astro, when using fetchContent /*.md on your markdown files, will expose to you their “content” which includes frontmatter, and an astro.headers section.

This component uses the headers information to create a list of divs and indent the header titles accordingly.

Astro.props.content

Component in Layout

So when you assign a layout to your markdown file

//-- /src/pages/mypost.md
---
layout: './../layouts/MyPostLayout.astro'
// ... (more frontmatter)
---

your markdown content will end up in the <slot/> on the layout file.

At the layout level, you can work with the Astro.props.content.astro.headings data and build a simple Table of Contents for your markdown file.

My Post layout setup contains:

//-- /src/layouts/MyPostLayout.astro
---
import BaseLayout from './BaseLayout.astro';
import TableOC from './../components/TableOC.vue';
const { content } = Astro.props;
---
<BaseLayout title={content.title} description={content.summary}>
  <link rel="stylesheet" href="/css/prism-atom-dark.css" />
  <div class="container post-md">
    <TableOC toc={content.astro.headings} />
    <slot />
  </div>
</BaseLayout>

I am passing the data in content.astro.headings into a TableOC.vue component.

TableOC component

I coded the component in vue because it will not require client-side interactivity and I am still not familiar with astro component syntax, conditionals feel weird at this point (for me). I ended up not using conditionals so… ¯\_(ツ)_/¯

//-- /src/components/TableOC.vue
<template>
  <div class="toc" v-if="toc.length">
    <div>Table of Contents</div>
    <div v-for="(n, idx) in toc" :key="'toc-'+idx">
      <span v-for="ii in n.depth-1" 
        :key="'i-'+idx+'-'+ii" 
        class="indent"
      >&nbsp;</span>
      <i class="fas fa-chevron-circle-right text-muted"></i>
      <a :href="'#'+n.slug">{{n.text}}</a>
    </div>
  </div>
</template>

<script>
export default {
  name: 'toc',
  props: {
    toc: { type: Array, required: true }
  }
}
</script>

<style scoped>
.toc { font-size:.8em; background:linear-gradient(#cdf,#fff,#f2f6ff); padding:10px; border-radius:5px; margin-bottom:20px; }
.toc a { text-decoration:none; }
.toc .indent { display:inline-block;width:15px; }
</style>

Logic

The logic is just an iteration of each header from the array sent in thru prop toc, and rendering a “space” to produce indentation based on the header’s depth property.

Each header item has properties depth, slug, and text.

Raw TOC

Here is this markdown file TOC:

[
  {
    "depth": 1,
    "slug": "introduction",
    "text": "Introduction"
  },
  {
    "depth": 2,
    "slug": "astropropscontent",
    "text": "Astro.props.content"
  },
  {
    "depth": 3,
    "slug": "component-in-layout",
    "text": "Component in Layout"
  },
  {
    "depth": 2,
    "slug": "tableoc-component",
    "text": "TableOC component"
  },
  {
    "depth": 3,
    "slug": "logic",
    "text": "Logic"
  }
]



Back to Post List
ok!