2023/08/202023/08/23
tocbotでZennとかQiitaみたいな目次を作る
tocbotというライブラリを使って、zennとかQiitaみたいな現在位置がハイライトされる目次を簡単に作る事ができたので、使い方をまとめておきます。
インストール
pnpm install tocbot rehype-slug
rehype-slug
を使って見出しにidを振る
rehype-slug
を使って、<h1>
や<h2>
などの見出し要素全てにidを振っておいてください。このブログではcontentlayerを使っているので、contentlayer.config.js
を以下のように変更しました
contentlayer.config.js
import { defineDocumentType, makeSource } from "contentlayer/source-files";
import remarkGfm from "remark-gfm";
import breaks from "remark-breaks";
import rehypeHighlight from "rehype-highlight";
import rehypeSlug from "rehype-slug";
/** @type {import('contentlayer/source-files').ComputedFields} */
const computedFields = {
slug: {
type: "string",
resolve: doc => `/${doc._raw.flattenedPath}`
},
};
export const Post = defineDocumentType(() => ({
name: "Post",
filePathPattern: `**/*.mdx`,
contentType: "mdx",
fields: {
title: {
type: "string",
required: true
},
description: {
type: "string"
},
date: {
type: "date",
required: true
}
},
computedFields
}));
export default makeSource({
contentDirPath: "./content",
documentTypes: [Post],
mdx: {
remarkPlugins: [remarkGfm, breaks],
rehypePlugins: [
rehypeHighlight,
rehypeSlug, // ここを追加
]
}
});
unified
などを使っている場合も、同じ要領でrehypeを追加してください
TOCコンポーネント
"use client";
import { useEffect } from "react";
import tocbot from "tocbot";
export const TableOfContents: React.VFC = () => {
useEffect(() => {
tocbot.init({
tocSelector: ".toc",
contentSelector: ".mdx-post", // 目次を抽出したい要素のクラス名
headingSelector: "h1, h2, h3, h5, h6",
scrollSmoothOffset: -60,
headingsOffset: 60,
scrollSmoothDuration: 300
});
return () => tocbot.destroy();
}, []);
return (
<div>
<h3 className="font-bold">
目次
</h3>
<div className="toc" />
</div>
);
};
init
で使用できるオプションの一覧は公式サイトにあります
スタイルを当てる
このブログで使っているCSS(tailwind)はこんな感じです。
デフォルトではリンクには.toc-link
、現在位置には.is-active-link
というクラス名が与えられるという事だけ覚えておけば、後は自由にCSSをあててスタイリングできます。
また階層を表現するために入れ子になっている.toc-list
の左側にmarginとborderを入れてます
.toc {
@apply py-2 pr-3;
.toc-list .toc-list {
@apply border-l-2 border-muted ml-5;
}
.toc-link {
@apply flex text-sm w-[100%] py-1.5 flex relative pl-9 hover:text-foreground text-muted-foreground;
&:before {
@apply content-[''] w-[10px] h-[10px] border-2 border-card rounded-full top-3 left-4 bg-card absolute shadow-[0_0_0_2px_rgb(var(--border))];
}
}
.is-active-link {
@apply font-bold text-foreground;
&:before {
@apply bg-primary outline-primary shadow-[0_0_0_2px_rgb(var(--primary))];
}
}
}