tocbotでZennとかQiitaみたいな目次を作る
2023/08/202023/08/23

tocbotでZennとかQiitaみたいな目次を作る

3 mins to read

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))];
    }
  }
}