2020-12-11

Gatsby.jsのブログにページネーションとタグを導入する

このサイトには入れてないんだけど、Gatsbyを使った別のサイトでページネーションとタグの機能を追加したので、せっかくなのでメモしておく

  • hakozaru.com => 次ページへ => hakozaru.com/page/2 みたいになる
  • タグの一覧でも hakozaru.com/tags/hoge => hakozaru.com/tags/hoge/2 みたいになる

のが目的
前提としてGatsby公式のgatsby-starter-blogを使っているものとする

ページネーション機能の追加

タグ機能の追加でページネーション機能を使いたいので、先にページネーション機能を追加していく
ページネーションに関してはパッケージに頼ることにした(別に自前実装でもよい)
このページを参考に導入していく

  1. yarnやnpmで gatsby-awesome-pagination をpackage.jsonへ追加する

  2. gatsby-node.jsimport { paginate } from 'gatsby-awesome-pagination'; する

  3. 再び gatsby-node.js をいじっていくが、おそらくGatsby.jsでブログを作成した時点で、すでに以下のようなクエリが書かれていて

    allMarkdownRemark(
      sort: { fields: [frontmatter___date], order: ASC }
      limit: 1000
    ) {
      nodes {
        id
        fields {
          slug
        }
      }
    }
    

    const posts = result.data.allMarkdownRemark.nodes という感じで記事の一覧が取得できるようになっていると思うので、この posts を使ってページネーションすればよい

    paginate({
      createPage,
      items: posts,
      itemsPerPage: 10,
      pathPrefix: ({ pageNumber }) => (pageNumber === 0 ? "/" : "/page"),
      component: path.resolve('src/templates/index.js')
    })
    

    注意点としては index.jstemplates/ 配下に移動させること

  4. 続いて templates/index.js で発行するGraphQLのクエリをページネーションに対応させる
    参考ページに書いてあるとおり $skip$limit を組み込む

    query ($skip: Int!, $limit: Int!) {
      allMarkdownRemark(
        sort: { fields: [frontmatter___date], order: DESC }
        skip: $skip   # <- これを追加
        limit: $limit # <- これを追加
      ) {
        ...
      }
    }
    
  5. ↑のクエリに書き換えると、コンポーネントに渡される引数に pageContext が追加され、ページネーションに関する情報が取得できるので、適宜ページ移動するリンクを設置していく

    const BlogIndex = ({ data, pageContext }) => {
      return(
        <div>
          ...
          <Link to={pageContext.previousPagePath}>前のページ</Link>
          <Link to={pageContext.nextPagePath}>次のページ</Link>
        </div>
      )
    }
    

    これで記事一覧のページネーションは完了
    続いてタグ毎の一覧のページネーションをやっていく

タグ機能の追加

  1. Gatsby.js公式スターターを使っている場合markdownで記事を作れるようになっていると思うので、まずは記事にタグ情報を追記する
    記事のFront matterに↓のような感じで追加する(以下はこの記事のFront matterを使った一例)

    ---
    title: Gatsby.jsのブログにページネーションとタグを導入する
    date: 2020-12-11
    slug: pagination-and-tags-with-gatsby
    tags: [Gatsby.js タグ ページネーション] # <- new!
    ---
    
  2. この状態で以下のクエリを発行すると

    {
      allMarkdownRemark {
        group(field: frontmatter___tags) {
          fieldValue
          totalCount
        }
      }
    }
    

    以下のような結果が得られる

    {
      "data": {
        "allMarkdownRemark": {
          "group": [
            {
              "tag": "Gatsby.js",
              "totalCount": 2
            },
            {
              "tag": "タグ",
              "totalCount": 1
            },
            ...
          ]
        }
      }
    }
    
  3. これだとどんなタグがあって、そのタグにどのくらい記事があるのかしかわからんので、さらに記事まで取得する
    具体的には以下のように nodes を追加する

    {
      allMarkdownRemark {
        group(field: frontmatter___tags) {
          fieldValue
          totalCount
          nodes {
            fields {
              slug
            }
          }
        }
      }
    }
    

    すると、各タグ毎の記事のリストが得られる

    {
      "data": {
        "allMarkdownRemark": {
          "group": [
            {
              "tag": "Gatsby.js",
              "totalCount": 2,
              "nodes": [
                {
                  "fields":  {
                    "slug": "/hogehoge/"
                  }
                },
                {
                  "fields":  {
                    "slug": "/fugafuga/"
                  }
                }
              ]
            },
            {
              "tag": "タグ",
              "totalCount": 1,
              "nodes": [
                {
                  "fields":  {
                    "slug": "/hogehoge/"
                  }
                }
              ]
            },
            ...
          ]
        }
      }
    }
    
  4. あとは通常の一覧のページネーションを作成する要領で、タグの一覧毎にページを生成してあげればよいので、 gatsby-node.js のGraphQLのクエリに↑の内容を組み込む

    tags: allMarkdownRemark(limit: 1000) {
      group(field: frontmatter___tags) {
        fieldValue
        nodes {
          fields {
            slug
          }
        }
      }
    }
    
  5. さらに gatsby-node.js でGraphQLのレスポンスからページを生成していく

    const tags = result.data.tags.group
    tags.forEach((tag) => {
      paginate({
        createPage,
        items: tag.nodes,
        itemsPerPage: 10,
        pathPrefix: `/tags/${tag.fieldValue}`,
        component: path.resolve('src/templates/tags.js'),
        context: {
          tag: tag.fieldValue
        }
      })
    })
    
  6. 最後に templates/tags.js で一覧に並べる記事をGraphQLのクエリでフィルタリングして完成

    query($skip: Int!, $limit: Int!, $tag: String!) {
      allMarkdownRemark(
        sort: { fields: [frontmatter___date], order: DESC }
        filter: {frontmatter: {tags: {in: [$tag]}}} # <- 該当するタグで絞りこむために必要
        skip: $skip   # <- ページネーションのために必要
        limit: $limit # <- ページネーションのために必要
        ) {
          ...
        }
      }
    }
    

    多分 index.js のクエリとは filter: { ... } の部分だけが異なるが、どうにも「$tagがあるときはこっちのクエリ、ないときはこっちのクエリ」と言う処理が良い感じに書く方法がわからなかったので、それぞれ専用のファイルを用意することにした
    この辺り良い感じにDRYにできる方法があれば教えていただきたい

    ちなみに僕は適当なフラグを用意して allMarkdownRemark に対して @include(if: XXX) をぶっかますというどうしようもない方法を思い浮かべたが、結局コード少なくならない上に見た目があまりに酷いのでやめた
    GraphQLは仕様をシンプルに保つために色々複雑なことはできないようなので、ある程度DRYについては割り切らないといけないところもあるのかもしれない(Rails脳)

まとめ

Gatsby.jsは公式の資料とコードいじったりして得た知識しかないので、今回の方法はベストな方法なのかはわからない
しかし割とコード量も増大しないで機能追加できたので個人的には満足している