チャベログ

easy-notion-blog のサイドメニューに目次を追加した

このブログのサイドメニューに追従する目次を追加しました。

tocbot

目次の追加には tocbot というライブラリを使用しました。

基本的な使い方は簡単で、まずはライブラリをインストールします。

今回はyarnを使うので以下のコマンドでインストールします。

yarn add tocbot

目次を追加するための基本的なコードは以下です。

import tocbot from 'tocbot';

tocbot.init({
  // 目次を追加するクラス名
  tocSelector: '.js-toc',
  // 目次を取得するクラス名
  contentSelector: '.js-toc-content',
  // 目次として取得するタグ
  headingSelector: 'h1, h2, h3',
});

tocbot.destroy();

目次を追加する

実際に tocbot を使用して目次を追加します。

まずは、tocコンポーネントを作成します。

import tocbot from 'tocbot'

  useEffect(() => {
    tocbot.init({
      tocSelector: '.js-toc',
      contentSelector: '.content',
      headingSelector: 'h4, h5, h6',
    })

    return () => tocbot.destroy()
  }, [])

  return (
      <div className="js-toc"></div>
  )
}
src\components\toc.tsx

上記のcontentSelectorで目次の取得先を.contentクラスとしているので、記事のコンテンツであるsrc\components\blog-parts.tsxPostBody<div className="content"></div>を追加します。

ちなみに easy-notion-blog では、Notion 内で記載した見出し1、見出し2、見出し3 は h4, h5, h6 に変換されているので、headingSelector: 'h4, h5, h6'と設定しています。

export const PostBody = ({ blocks }) => (
  <div className="content">
    <div className={styles.postBody}>
      <NotionBlocks blocks={blocks} />
    </div>
  </div>
)
src\components\blog-parts.tsx

これで目次を埋め込む準備はできたので、目次を入れたい場所に<Toc />を追加します。

<div className={styles.subContent}>
  <BlogPostLink
    heading="Posts in the same category"
    posts={sameTagPosts}
  />
  <BlogPostLink heading="Recommended" posts={rankedPosts} />
  <BlogPostLink heading="Latest posts" posts={recentPosts} />
  <BlogTagLink heading="Categories" tags={tags} />
  <Toc />
</div>
src\pages\blog\[slug].tsx

ちなみに CSS は tocbot の公式が用意してくれているので使ってみます。

以下のようにsrc\components\document-head.tsxで cdn を 読み込めば CSS が適応されます。

{NEXT_PUBLIC_URL ? (
        <link
          rel="canonical"
          href={new URL(asPath, NEXT_PUBLIC_URL).toString()}
        />
      ) : null}
		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.11.1/tocbot.css">
    </Head>
  )
}

export default DocumentHead

これで目次をサイドメニューに追加できました。

ただし、追従がうまくいっていないのと、目次のリンクを押しても一番上に戻ってしまいます。

見出しタグに id を付ける

上記の原因は見出しタグに id がついていなかったことでした。

ということで、フロント側でh4, h5, h6を取得して、idを付けていきます。

import { useEffect } from 'react'
import tocbot from 'tocbot'
import styles from '../styles/toc.module.css'

export default function Toc() {
  const addIdsToTitle = () => {
    const entryContainer = document.querySelector('.content')
    if (!entryContainer) {
      return
    }
    const headings = entryContainer.querySelectorAll('h4, h5, h6');

    [].forEach.call(headings, (heading: HTMLElement) => {
      const id = heading.textContent
      if (!heading.getAttribute('id')) {
        heading.setAttribute('id', id)
      }
    })
  }

  const isHeadingsExists = () => {
    const entryContainer = document.querySelector('.content')
    if (!entryContainer) {
      return
    }
    const headings = entryContainer.querySelectorAll('h4, h5, h6')
    if (headings.length === 0) {
      return false
    }
    return true
  }

  useEffect(() => {
    addIdsToTitle()
    const item = document.querySelector('.js-toc') as HTMLElement
    if (!item) {
      return
    }
    if (!isHeadingsExists()) {
      return
    }
    item.style.display = 'block'
    tocbot.init({
      tocSelector: '.js-toc',
      contentSelector: '.content',
      headingSelector: 'h4, h5, h6',
    })

    return () => tocbot.destroy()
  }, [])

  return (
    <div className={styles.tocbox}>
      <div className="js-toc"></div>
    </div>
  )
}
src\components\toc.tsx

const id = heading.textContenth4, h5, h6タグに各見出し名と同じ id 名を付けることで、目次の追従とリンクが上手く動作するようになりました。

ちなみに css はtocbot が提供してくれている cdn を使わずに、自前でシンプルなデザインに変更しました。

.tocbox {
    position: sticky;
    margin-top: 50px;
    top: 30px;
}

.tocbox :global .toc-list {
    padding-left: 1.5rem;
    padding-top: 0.1rem;
    list-style-type: none;
}

.tocbox :global .toc-link {
    color: rgba(0, 0, 0, 0.6);
}

.tocbox :global .is-active-link {
    color: rgba(0, 0, 0, 0.87);
    font-weight: 700;
}
src\styles\toc.module.css
参考

今回、参考にさせていただいたリポジトリです。

終わりに

リンクがうまくいかない過程で、easy-notion-blog の開発者のおとよさんからアドバイスをいただきました。

easy-notion-blog ではすでに Notion の 目次ブロックに対応しており、既につけられている id を使えばもっと効率よく対応できたかもしれません。

(tocbot を使わずに自前でサイドバーに目次を追加する場合は上記のコードをそのまま参考にすればできる気がしますが、tocbot を使う場合に上記のような方法で対応する方法がわかりませんでした。。。)

easy-notion-blog がデフォルトで提供している機能はできるだけ使っていきたいので、別の方法が分かれば対応したいと思っています。