Gatsbyのカスタマイズ - マークダウン記事の目次表示

実現したいこと

マークダウンで記述した記事の冒頭に、目次を自動的に表示したい。

ToC in the Header

記事の見出しにはリンクを自動的に生成して、目次からその見出しへ飛べるようにする。

Header with a Link

2 カラムレイアウトでは、サイドバーにも目次を自動的に表示する。サイドバーの目次はスティッキーにする。記事の閲覧箇所に関係なく 目次が常に表示 されようにして、更なる閲覧性の向上を図りたい。

ToC in the Sidebar

前提条件

既に React-Bootstrap が適用済みで、2 カラムレイアウトが作成済である事を前提とする。React-Bootstrap の適用方法は こちらの記事 で、2 カラムレイアウトの作成方法は こちらの記事 で説明している。Gatsby Starter Blog を利用しているが、他のスタータでも大きな違いはないはず。

実装

基礎

最も大切なことは gatsby-transformer-remark が、GraphQL で HTML 形式の目次を提供 していること。GraphQL で tableOfContents(absolute: false, maxDepth: 3) などと記述すると、dangerouslySetInnerHTML で描画可能な目次が得られる。

もう一つは、gatsby-remark-autolink-headers プラグインを導入すると、マークダウン記事の見出しにホーバーリンクを自動生成してくれること。

この2つを組み合わせれば、目次の表示と、目次から見出しへ飛ぶ機能が簡単に実現できる。素晴らしい!!

Toc コンポーネントの作成

GraphQL で取得した目次をプロパティで受け取って表示する Toc コンポーネント (src/components/Toc.js) を作成する。

const Toc = ({ toc }) => {
  // falsy: '', null, ...
  if (!toc) return '';

  return (
    <div className={`mb-4 p-3 bg-light ${styles.toc}`}>
      <h2>目次</h2>
      <div dangerouslySetInnerHTML={{ __html: toc }} itemProp="articleBody" />
    </div>
  );
};

Toc コンポーネントの配置

マークダウン記事の冒頭に目次が表示されるように、 src/templates/blog-post.js に Toc コンポーネントを挿入する。

<Layout snip... >
  <Seo snip... />
  <article itemScope itemType="http://schema.org/Article">
  <header className="mb-4">
    snip...
  </header>
  <Toc toc={post.tableOfContents} />
  <section
    className={styles.blogPost}
    dangerouslySetInnerHTML={{ __html: post.html }}
    itemProp="articleBody"
  />
  <hr />
  </article>
  snip...
</Layout>

ちなみに、記事の任意の場所に目次が配置できる gatsby-remark-table-of-contents もあるが、今回は使っていない。

次にサイドバーにも Toc コンポーネントを配置する。Layout コンポーネント (src/components/Layout.js) の、Tags コンポーネント(タグ一覧)の後ろに配置する。目次をスティッキーにするために、Bootstrap の sticky-top クラスを指定してる事に注目!

const Layout = ({ children, toc }) => {
  const data = useStaticQuery(graphql`
    query {
      site {
        siteMetadata {
          title
        }
      }
    }
  `);

  const siteTitle = data.site.siteMetadata?.title || `Title`;

  return (
    <>
      <NavBar siteTitle={siteTitle} />
      <Container>
        <Row>
          <Col lg={8}>
            <main>{children}</main>
          </Col>
          <Col lg={4}>
            <Bio />
            <Tags />
            <div className="sticky-top">
              <Toc toc={toc} />
            </div>
          </Col>
        </Row>
        <footer className="py-4">
          © {new Date().getFullYear()}, Built with
          {` `}
          <a href="https://www.gatsbyjs.com">Gatsby</a>
        </footer>
      </Container>
    </>
  );
};

注意事項

  • gatsby-remark-prismjs と共存する場合は、gatsby-remark-autolink-headers を gatsby-remark-prismjs の前(上)に配置する
  • gatsby-remark-table-of-contents で目次を表示するときは、maintainCase: false の指定が必要らしい (未検証)

gatsby-transformer-remark が生成する目次のスタイリング

箇条書きの 一段目だけ li の中に p が含まれてて、目次が 部分的に行間が空いて 間抜けに表示される。これ僕だけ?

ToC with Wrong Margin

そこで Toc コンポーネントのスタイル (src/components/Toc.module.css) で、p { margin-bottom: 0rem; } と指定して修正する。

参考リンク