Yukarinoki's Blog

gatsby-blogにtable of contentを付ける

November 19, 2020

概要

gatsbyで作られたブログに

  • リンクが機能していて
  • スクロールに追従する
    目次をつける方法について

MDX pluginは使わずにgatsby-transformer-remarkの元で実現しています。

graphQLでtable of contents

gatsby-transformer-remarkを使うと、gatsbyのgraphQLにいろいろな項目が追加されますが、そのなかにtable of contentsがあります。

まず、これをblog postのtemplateから読み取りましょう。

template/blog-post.js
export const pageQuery = graphql`
  query BlogPostBySlug($slug: String!) {
    site {
      siteMetadata {
        title
      }
    }
    markdownRemark(fields: { slug: { eq: $slug } }) {
      id
      excerpt(pruneLength: 160)
      html
      tableOfContents //←これ追加
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
      }
    }
  }
`

そして、得られたtableOfContentsを(ここらへんは個人の実装のより異なりますが)、自分の作ったtableOfContents用のcomponentに渡します。

templates/blog-post.js
const BlogPostTemplate = ({ data, pageContext, location }) => {
  const post = data.markdownRemark
  const siteTitle = data.site.siteMetadata.title
  const { previous, next } = pageContext

  return (
    <Layout 
      location={location} title={siteTitle}
      rightSide={
                  <>
                    <Bio/> 
                    <TOC tocitems={data.markdownRemark.tableOfContents}/> // <- これ
                  </>
                }
     > 
....................中略..................
)

うけとる、TOC componentはこんな感じ。tableOfContentsは生のhtmlなのでdengerouslySetInnerHTMLで渡します。

components/toc.js
import React from "react"
import styled from "styled-components"
import { rhythm, scale } from "../utils/typography"

const TOCInner = ({ className, tocitems }) => {
    return (
        <div classname={className} dangerouslySetInnerHTML={{__html: tocitems}} />
    )
}

const TOC = styled(TOCInner)`
padding: ${rhythm(0.5)} ${rhythm(0.25)};
padding-top: ${rhythm(0.5)}
font-style: italic;
color: var(--fg-demisub-color);
${scale(-3 / 8)};
& > a {
  line-height: 0;
}
`
export default TOC

この時点で見た目だけまともなのができますが、まだpage内リンクは機能していません (ハリボテ・・・)

page内リンクが機能するようにする

上で作ったtable of contentsが動かないのは、page内の<h1> ~ <h6>idが設定されていないからです。 なのでidを設定するためにpluginを入れます。

idを設定してくれる(と嘯いている) pluginはいくつかありますが、
gatsby-remark-autolink-headers を使ってください。
https://www.gatsbyjs.com/plugins/gatsby-remark-autolink-headers/

gatsby-remark-sluggatsby-remark-heading-slugはgraphQLのtableOfContentsがなにも返さなくなるので使わないように

npm i gatsby-remark-autolink-headers
gatsby-config.js
resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          `gatsby-remark-code-titles`,
          `gatsby-remark-autolink-headers`, // <- これ追加
          {
            resolve: `gatsby-remark-images`,
            options: {
              maxWidth: 590
            }
          },

そうするとリンクが機能するようになります。

スクロールに目次が追従するようにする

(僕なんかからすると)モダンなデザインに思えますが、現在ではcssにposition: stickyを設定するだけでこのような動作を実現することができます。

toc.js
const TOC = styled(TOCInner)`
top: 0px;
padding: ${rhythm(0.5)} ${rhythm(0.25)};
padding-top: ${rhythm(0.5)};
margin-top: ${rhythm(0.5)};
margin-left: ${rhythm(0.20)};
margin-right: ${rhythm(0.20)};
position: sticky; // <-これ
........

しかし、単純にposition: stickyを設定するだけでは、うまく動かない場合がしばしばあります。

具体的には、

  • 親要素のどれかに、overflow: hidden overflow: auto が設定されている。
  • その要素が動ける範囲がない
  • その要素が固定される位置が明示されていない
    などの場合があります。

僕の場合も動かず、原因は3番目でした。

要素の位置を明示するために

toc.js
const TOC = styled(TOCInner)`
top: 0px; // <- これ
padding: ${rhythm(0.5)} ${rhythm(0.25)};
padding-top: ${rhythm(0.5)};
margin-top: ${rhythm(0.5)};
margin-left: ${rhythm(0.20)};
margin-right: ${rhythm(0.20)};
position: sticky; 
........

を指定したら動きました。

まとめ

CSSがうまく動かない系バグはエラーメッセージがないので原因究明が大変です。position: stickyが動かない 理屈 も分かり次第追記しようと思います。


Written by yukarinoki
© 2022, Built with