Logo
Published on
ยท10 min read

Adding Tocbot to a Next.js Blog

I apologize in advance for any awkward expressions in English. ๐Ÿ™

English is not my native language, and I have relied on ChatGPT's assistance to proceed with the translation.

Overview

When reading articles, having a table of contents is quite useful. Moreover, it would be even more convenient if the table of contents remained visible as I scrolled down. To achieve this, I utilized the Tocbot library.

It's worth mentioning that the Tailwind CSS Blog Starter template already includes a table of contents within the content. In addition to this, I used Tocbot to add a fixed table of contents to the right side of the page.

In summary:

  • On screens with a width of 1280px or more, the table of contents within the content is hidden, and a fixed table of contents appears on the right side.
  • On screens with a width of less than 1280px, the table of contents within the content is visible, while the fixed table of contents on the right side is hidden.

The image below illustrates the left side showing the table of contents for screens 1280 pixels or more and the right side displaying the screen less than 1280 pixels.

Table of Contents by Resolution

Add Tocbot

Tocbot Install

npm install --save tocbot

Adding the Tocbot Component

Add /components/Tocbot.js with the following content:

import { useEffect } from 'react'
import tocbot from 'tocbot'

const TocSide = () => {
  useEffect(() => {
    tocbot.init({
      tocSelector: '.toc',
      contentSelector: 'article',
      headingSelector: 'h2, h3',
      ignoreSelector: '.toc-ignore',
    })
    return () => tocbot.destroy()
  }, [])

  return (
    <div>
      <div className="lg-block hidden pt-6 pb-10 text-gray-500 dark:text-gray-400 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700">
        <span className="font-bold text-gray-600  dark:text-gray-300">Table of Contents</span>
        <div className="toc"></div>
      </div>
    </div>
  )
}

export default TocSide
  • Set it to hidden by default and use lg:block to make it visible on screens larger than 1280px.
  • Define lg:block in the tocbot.css file below.

Adding Tocbot CSS

.toc {
  overflow-y: auto;
}
.toc > .toc-list {
  overflow: hidden;
  position: relative;
  font-size: 0.9em;
  margin-top: 16px;
}
.toc > .toc-list li {
  list-style: none;
  line-height: 2em;
}
.js-toc {
  overflow-y: hidden;
}
.toc-list {
  margin: 0;
  padding-left: 10px;
}
a.toc-link {
  /*color:currentColor;*/
  height: 100%;
}
.is-collapsible {
  max-height: 1000px;
  overflow: hidden;
  transition: all 300ms ease-in-out;
}
.is-collapsed {
  max-height: 0;
}
.is-position-fixed {
  position: fixed !important;
  top: 0;
}
.is-active-link {
  font-weight: 700;
  color: #37c3b3;
}
.toc-link::before {
  background-color: #e5e5e5;
  content: ' ';
  display: inline-block;
  height: inherit;
  left: 0;
  margin-top: -1px;
  position: absolute;
  width: 2px;
}
.is-active-link::before {
  background-color: #37c3b3;
}
/*
.toc-link:hover {
  color: white;
}
*/

@media (min-width: 1280px) {
  .lg-block {
    display: block;
  }
}

@media (max-width: 1280px) {
  .sm-show {
    display: block;
  }
}
  • .lg-block class to display it on screens with a width of 1280px or more.
  • .sm-show class to display it on screens with a width of less than 1280px.

Adding a Layout to Display Tocbot on the Right Side

  • Added /layouts/PostToc.js with the following content:
import PageTitle from '@/components/PageTitle'
import SectionContainer from '@/components/SectionContainer'
import { BlogSEO } from '@/components/SEO'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import Comments from '@/components/comments'
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
import TocSide from '@/components/Tocbot'

const postDateTemplate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }

export default function PostLayout({ frontMatter, authorDetails, children }) {
  const { slug, date, title, tags } = frontMatter

  return (
    <SectionContainer>
      <BlogSEO
        url={`${siteMetadata.siteUrl}/blog/${slug}`}
        authorDetails={authorDetails}
        {...frontMatter}
      />
      <ScrollTopAndComment />
      <article>
        <div className="xl:divide-y xl:divide-gray-200 xl:dark:divide-gray-700">
          <header className="pt-6 xl:pb-6">
            <div className="space-y-1 text-center">
              <dl className="space-y-10">
                <div>
                  <dt className="sr-only">Published on</dt>
                  <dd className="text-base font-medium leading-6 text-gray-500 dark:text-gray-400">
                    <time dateTime={date}>
                      {new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
                    </time>
                  </dd>
                </div>
              </dl>
              <div>
                <PageTitle>{title}</PageTitle>
              </div>
            </div>
          </header>
          <div
            className="divide-y divide-gray-200 pb-8 dark:divide-gray-700 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0"
            style={{ gridTemplateRows: 'auto 1fr' }}
          >
            <div className="divide-y divide-gray-200 dark:divide-gray-700 xl:col-span-3 xl:row-span-2 xl:pb-0">
              <div className="prose max-w-none pt-10 pb-8 dark:prose-dark">{children}</div>
              <Comments frontMatter={frontMatter} />
            </div>
            <footer style={{ position: 'sticky', top: '32px' }}>
              <TocSide />
              <div className="divide-gray-200 text-sm font-medium leading-5 dark:divide-gray-700 xl:col-start-1 xl:row-start-2 xl:divide-y">
                {tags && (
                  <div className="py-4 xl:py-8">
                    <h2 className="toc-ignore text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
                      Tags
                    </h2>
                    <div className="flex flex-wrap">
                      {tags.map((tag) => (
                        <Tag key={tag} text={tag} />
                      ))}
                    </div>
                  </div>
                )}
              </div>
            </footer>
          </div>
        </div>
      </article>
    </SectionContainer>
  )
}

Modifications for Applying Tocbot

Editing the TocInline Component

Modify the default TocInline component found in the Tailwind CSS Blog template.

  • Add className="sm-show hidden" to the details tag.
    (This ensures it's only visible on screens smaller than 1280px.)
  • This change will apply when using asDisclosure. Otherwise, you can apply it to the ul tag as needed.
<details open className="sm-show hidden">
  <summary className="ml-6 pt-2 pb-2 text-xl font-bold">Table of Contents</summary>
  <div className="ml-6">{tocList}</div>
</details>

Apply CSS Modifications

  • Import tocbot.css in /pages/_app.js to ensure the CSS modifications are applied.
import '@/css/tocbot.css'

Apply Layout When Writing Posts

To apply the layout to individual posts, add the layout property to the front matter of each post's markdown file:

---
...omitted...
layout: PostToc
---

If you want to apply this layout as the default for all posts, you can modify the DEFAULT_LAYOUT in the /pages/blog/[...slug].js file.

const DEFAULT_LAYOUT = 'PostToc'
  • When adding a layout file, if you encounter module not found errors, try stopping and restarting the development server (npm start).

  • To display the table of contents within the content, you need to add the <TocInline /> component separately in the content. It's not mandatory, but if it's not added, the table of contents won't be visible on screens smaller than 1280px.

<TOCInline toc={props.toc} exclude="Overview" asDisclosure />

Conclusion

I have removed sections such as next/previous posts and author information, which I considered unnecessary.

You can refer to PostLayout to add any necessary content.

You can review the code on the add_tocbot branch or by checking the commit history on GitHub.