このブログのサイドメニューに追従する目次を追加しました。
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>
)
}
上記のcontentSelector
で目次の取得先を.content
クラスとしているので、記事のコンテンツであるsrc\components\blog-parts.tsx
のPostBody
に<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>
)
これで目次を埋め込む準備はできたので、目次を入れたい場所に<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>
ちなみに 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>
)
}
const id = heading.textContent
でh4, 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;
}
参考
今回、参考にさせていただいたリポジトリです。
終わりに
リンクがうまくいかない過程で、easy-notion-blog の開発者のおとよさんからアドバイスをいただきました。
easy-notion-blog ではすでに Notion の 目次ブロックに対応しており、既につけられている id を使えばもっと効率よく対応できたかもしれません。
(tocbot を使わずに自前でサイドバーに目次を追加する場合は上記のコードをそのまま参考にすればできる気がしますが、tocbot を使う場合に上記のような方法で対応する方法がわかりませんでした。。。)
easy-notion-blog がデフォルトで提供している機能はできるだけ使っていきたいので、別の方法が分かれば対応したいと思っています。