Gatsbyで超絶スムーズに動く目次のコンポーネント実装する


こんな感じで、目次をクリックすると、超絶スムーズに目的地に到達する機能を実装します。

実際の動作は本家サイトgenkitech.netでご覧ください(まだ存在していたら)

やりたかったこと

  1. 好きな場所に配置できる目次コンポーネントの作成
  2. URLにpage#id のように#を追加せず、今のURLのままジャンプ(選択可)
  3. スムーズなスクロールUI

作り方

基本的なGatsbyの知識を前提とします。

必要なプラグイン

以下のプラグインを使用しているためインストールします。

  • gatsby-remark-autolink-headers(hタグにジャンプするためのIDを埋め込む)

  • gatsby-plugin-anchor-links(URLを変えずにジャンプするコンポーネントなど)

gatsbyの設定

gatsby-config.jsに以下の設定を追加します。

gatsby-config.js
...
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          ...
          {
            resolve: `gatsby-remark-autolink-headers`,
            options: {
              icon: false, //Hタグ横のアイコンを消す場合false
            },
          },
          ...
        ]
      }
    },
    {
       resolve: "gatsby-plugin-anchor-links",
       options: {
            offset: -100 //スクロール先のオフセット
       }
    },
...        

コンポーネントを作成する

以上でHタグにIDが埋め込まれ、<AnchorLink>コンポーネントを配置するとジャンプするようになったので、以下の目次表示コンポーネントを作成して画面に追加します。

下の方の sx={{}} の所は「theme-ui」の機能を使ってスタイリングしているだけなので、上部のimportと一緒にとりあえず外してしまっても大丈夫です。

/** @jsx jsx */
import { jsx } from "theme-ui"
import React from 'react';
import { graphql, StaticQuery } from 'gatsby';
import { AnchorLink } from "gatsby-plugin-anchor-links";

export const TableOfContents: React.FC<{
  className?: string, 
  slug: string,
}> = props => (
  <StaticQuery
    query={graphql`
      {
        allMarkdownRemark {
          edges {
            node {
              headings {
                depth
                id
                value
              }
              fields {
                slug
              }
            }
          }
        }

      }
    `}
    render={(data: any) => {
      //graphqlでは全件取得しているため対象を特定
      const headings = data.allMarkdownRemark.edges.find((n:any)=>{
        return n.node.fields.slug == props.slug })?.node?.headings;

      return (headings && <div sx={{display: 'grid',gap: 2}}>
        {headings.map((x:any)=><AnchorLink         
            className={props.className??'' + ' toc-link toc-depth-'+x.depth}

            //末尾にスラッシュを付ける場合はこっち
            to={"/" + props.slug + '#' + x.id}

            //末尾にスラッシュを付けない場合はこっち
            //to={"/" + props.slug.replace('/','') + '#' + x.id}

            stripHash //#表示を追加しない

            sx={{     //theme-uiでスタイルを設定
              display: 'block',
              ml: ((Number(x.depth)-2)*15)+'px',
              textDecoration: 'none',
              fontSize:'0.95rem',
              lineHeight: '1.1rem',
              '&:hover': { textDecoration: 'underline' }
            }}
          >
            {x.value}
          </AnchorLink>
        )}
      </div>);
    }}
  />
);

使い方と解説

上の<TableOfContents>コンポーネントの例では、slugをキーにして対象のドキュメントを検索するため、post.jsなどの記事を表示しているコンポーネントで以下のようにすると

<TableOfContents slug={post.fields.slug} />

好きなところに目次ジャンプ機能を挿入できます。

補足

好きなところにコンポーネントを配置できるようするため、親からgraphqlの情報を受け取らず、StaticQueryで全件取得して対象だけ使用しています。(gatsbyの画像でよくやる方法と同じです)

ただこの方法は、本番環境では影響がないものの、「gatsby develop」で開発してホットリロードされているときに、ブラウザでF5などで更新すると、develop開始時の情報に見た目上戻ってしまう事があったりします。

再びホットリロードさせるか、developコマンドを再実行すると最新情報で表示されるようです。