ホームGatsby【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その5)Gatsbyブログにページネーションを実装しました。
2020年5月23日

【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その5)Gatsbyブログにページネーションを実装しました。

book

10月28日追記
Gatsbyブログについて、初noteを書きました。
【完全版】爆速GatsbyでWordPressちっくなブログを作る全手順

こんにちは、筋肉めがねです。

ドイツでは、Covid-19に関する規制が少しずつ緩和されてきており、街が活気を取り戻してきました。ですが、あまり気を緩める事なく、Post coronaの新しい形での生活スタイルを確立していけたら良いですね。

さて、このGatsbyブログですが、これまでは、トップページに全ての記事の見出しを載せる形で表示しており、とても縦に長いサイトでした。記事の数が30に到達する頃でしたので、昔の記事と最近の記事を行ったり来たりするには、上にスクロール、下にスクロールと、あまりユーザーエクスペリエンスがよろしくないサイトでした。そこで、1ページ毎に6記事の見出しを表示し、Googleの検索結果に出てくるようなページネーションを実装することで、新しい記事、古い記事へサクサクっとアクセスできるようサイトを改修しました。

記事タイトルにある通り、この記事は、「Gatsbyブログのデザインをワードプレスちっくにする手順」シリーズの、その5です。

その1からその4までの記事はこちらです。

【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その1)記事のテンプレートを作り込む。

【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その2)「この記事書いた人」っていうComponent作成とサイドバーの一部をスマホで非表示、そして背景色を変更しました。

【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その3)ヘッダーとフッターを追加しました。

【連載】Gatsbyブログのデザインをワードプレスちっくにする手順(その4)twitterシェアボタンとtwitter cardを導入しました。

それでは、早速進めていきましょう。

この記事では以下の内容について、書いていきます。

blog-list-template.jsxを追加する。

このブログでは、gatsby-starter-kitの1つである、“gatsby-v2-starter-lumen”を使っておりますが、このstarter kitでは、/src/pages直下にデフォルトでindex.jsxファイルがあります。

Gatsby公式ページの「Adding Pagination」を参考にしながら、はじめのうちは、この/src/pages/index.jsxをゴニョゴニョ変更し、試してみましたが、結局うまくいきませんでした。

その時のissueはこちらです。
Variable “$skip” of required type “Int!” was not provided

それで、結果的に動いた方法は、/src/templates/直下に”blog-list-template.jsx”というファイルを作り、そちらのページを、トップページとして読み込ませる、という事でした。

注意点とし、“blog-list-template.jsx”ファイルを作っただけでは、ページネーションは動作しません。gatsby-node.jsを更新した後に、ページネーションが正しく動作するので、それまでは、/src/pages/index.jsxは削除せずに、そのままにしておきましょう。

こちらが、blog-list-template.jsxです。基本的には、starter-kitである”gatsby-v2-starter-lumen”の/src/pages/index.jsxの内容をコピペすれば良いのですが、幾つか変更を加えております。

/src/templates/blog-list-template.jsx
import React from 'react'
import Helmet from 'react-helmet'
import { graphql } from 'gatsby'
import Layout from '../components/Layout'
import Post from '../components/Post'
import Sidebar from '../components/Sidebar'
import { Link } from 'gatsby'
import { blue } from 'color-name'

class BlogList extends React.Component {
  render() {
    const items = []
    const { title, subtitle } = this.props.data.site.siteMetadata
    const posts = this.props.data.allMarkdownRemark.edges

    // 以下のcurrentとtotalがとても大事です。
    // currentは、現在何ページ目にいるか、という事ですが、ブログのトップページであればcurrentは1であり、全部で5ページある、という事であれば、totalは5となります。

    const {
      current,
      total,
    } = this.props.pageContext

    // ここで、itemsというArrayに、1記事1記事の情報を格納しております。
    posts.forEach(post => {
      items.push(<Post data={post} key={post.node.fields.slug} />)
    })

    return (
      <Layout>
        <div>
          <Helmet>
            <title>{title}</title>
            <meta name="description" content={subtitle} />
          </Helmet>
          {/* commented out by kinnniku <Sidebar {...this.props} /> */}
          <div className="content">
            <div className="content__inner">{items}</div>
            
            {/* これ以降のul, liでページネーションを表示しています。 */}
            <ul style={{
              display: `flex`,
              flexWrap: `wrap`,
              justifyContent: `center`,
              listStyle: `none`,
              paddingLeft: 30,
              paddingRight: 30,
              margin: `0 auto`,
            }}
            className="pagination">

                <li className={current === 1 ? 'disabled' : ''}>
                  <Link to={'./'}>1</Link>
                </li>

                <div className={current === total || current === total - 1 ? '' : 'disabled'}>
                  ...
                </div>

                <li className={current === 1 || current === 2 ? 'disabled' : ''}>
                  <Link to={`/page/${current -1}`}>{current-1}</Link>
                </li>

                <li style={{backgroundColor:`#5d93ff`}}>
                  <Link style={{color:`white`}} to={`/page/${current}`}>{current}</Link>
                </li>

                <li className={current === total || current === total - 1 ? 'disabled' : ''}>
                  <Link to={`/page/${current +1}`}>{current+1}</Link>
                </li>

                <div className={current === 1 || current === 2 ? '' : 'disabled'}>
                  ...
                </div>

                <li className={current === total ? 'disabled' : ''}>
                  <Link to={`/page/${total}`}>{total}</Link>
                </li>
            </ul>

          </div>
          <Sidebar {...this.props} />
        </div>
      </Layout>
    )
  }
}

export default BlogList

export const pageQuery = graphql`

  # gatsbyの公式ページにある通り、$skip: Int!, $limit: Int!を設定します。
  query blogListQuery($skip: Int!, $limit: Int!){
    site {
      siteMetadata {
        title
        subtitle
        copyright
        menu {
          label
          path
        }
        author {
          name
          twitter
          github
        }
      }
    }
    allMarkdownRemark(
      #デフォルとでは、limit: 1000になっているので、それを$limitに変更します。
      limit: $limit
      filter: { frontmatter: { layout: { eq: "post" }, draft: { ne: true } } , fields: { draft: { eq: false } } }
      sort: { order: DESC, fields: [frontmatter___date] } 
      # skip: $skipを追記します。
      skip: $skip
      ) {
      edges {
        node {
          fields {
            slug
            categorySlug
          }
          frontmatter {
            title
            featuredImage {
              childImageSharp {
                sizes(maxWidth: 630) {
                  ...GatsbyImageSharpSizes
                }
              }
            }
            path
            date
            category
            description
          }
        }
      }
    }
  }
`

これで、blog-list-template.jsxの設定が終わりました。続いて、gatsby-node.jsです。

gatsby-node.jsを変更する。

変更したgatsby-node.jsは以下のとおりです。サイトが読み込まれる時に、gatsby-node.jsが読み込まれ、その中でcreatePageというfunctionが呼ばれ、ページが作られていく、という仕組みです。

gatsby-node.jsの変更箇所については、コード内にコメントしておりますが、特に大事な箇所については、コードの下に、注意点として書いておきます。

gatsby-node.js
const _ = require('lodash')
const Promise = require('bluebird')
const path = require('path')
const slash = require('slash')

exports.createPages = ({ graphql, actions }) => {

  const { createPage } = actions

  return new Promise((resolve, reject) => {
    const postTemplate = path.resolve('./src/templates/post-template.jsx')
    const pageTemplate = path.resolve('./src/templates/page-template.jsx')
    const tagTemplate = path.resolve('./src/templates/tag-template.jsx')
    
    // ここで先ほど作成したblog-list-template.jsxを読み込んで、blogPostListという変数に入れます。
    const blogPostList = path.resolve('./src/templates/blog-list-template.jsx')

    const categoryTemplate = path.resolve('./src/templates/category-template.jsx')

    graphql(`
      {
        allMarkdownRemark(
          # ここのlimitはデフォルトの1000で問題ありません。
          limit: 1000
          filter: { frontmatter: { draft: { ne: true } } , fields: { draft: { eq: false } } }
          # 注意点1(説明はコード下の説明文を参照) sortは大事。
          sort: { order: DESC, fields: [frontmatter___date] }
        ) {
          edges {
            node {
              fields {
                slug
              }
              frontmatter {
                #デフォルトでtitleが入っていなければ、titleを追加しましょう。
                title
                tags
                layout
                category
              }
            }
          }
        }
      }
    `).then(result => {
      if (result.errors) {
        console.log(result.errors)
        reject(result.errors)
      }

      const posts = result.data.allMarkdownRemark.edges

      // 注意点2(説明はコード下の説明文を参照) 新たにcountという変数を定義。

      let count = 0
      posts.forEach((post) => {
        if(post.node.fields.slug.includes('/posts')){
          
          count = count + 1
        } else{

        }
      })

      // このpostsPerPageで、1ページに何記事の見出しをリストアップするのか、設定します。

      const postsPerPage = 6
      
      // 注意点3(説明はコード下の説明文を参照) 記事数、そして1ページ毎の記事数(ここでは6)から、全体のページ数を算出します。
      let numPages = Math.ceil(count / postsPerPage)

      // ここで、複数のページを作ります。各ページ毎に6記事の見出しがリストアップされます。
      Array.from({ length: numPages }).forEach((_, index) => {
        const withPrefix = pageNumber => pageNumber === 1 ? `/` : `/page/${pageNumber}`
        const pageNumber = index + 1
        createPage({
          path: withPrefix(pageNumber),

          // 上で作成したblogPostList変数を使用します。
          component: blogPostList,
          context: {
            limit: postsPerPage,
            skip: index * postsPerPage,
            current: pageNumber,
            total: numPages,
            hasNext: pageNumber < numPages,
            nextPath: withPrefix(pageNumber + 1),
            hasPrev: index > 0,
            prevPath: withPrefix(pageNumber - 1),
          },
        })
      })

      //ここで1記事1記事、記事毎のページを作ります。

      let postsBlog = []
      posts.forEach((post) => {    
        
        // /postsというフィルターをかけることで、ブログの記事に対してのみ、ページを作成します。
        // 例えば、このブログには、profileというページがありますが、ここでは、profile, portfolioといった「ページ」は作成されません。 

        if(post.node.fields.slug.includes('/posts')){
          postsBlog.push(post)
        } else{
        }
      })

      postsBlog.forEach((post, index) => {
          const previous = index === postsBlog.length - 1 ? null : postsBlog[index + 1].node
          const next = index === 0 ? null : postsBlog[index - 1].node

          // 実際に各記事のページを作成しているのは、以下のfunctionです。
          createPage({
            path: `${post.node.fields.slug}`,
            component: postTemplate,
            context: {
              slug: post.node.fields.slug,
              previous,
              next,
            },
          })
      })

      _.each(result.data.allMarkdownRemark.edges, edge => {
        // 上で、各記事ごとのページを作成したので、残りは、profile, portfolioなどの「ページ」を作成します。

        if (_.get(edge, 'node.frontmatter.layout') === 'page') {
          createPage({
            path: edge.node.fields.slug,
            component: slash(pageTemplate),
            context: { slug: edge.node.fields.slug },
          })
        } else if (_.get(edge, 'node.frontmatter.layout') === 'post') {

          {/* ここは、starter-kitにデフォルトで入っている箇所ですが、既に、各記事ごとのページは作成したので、この部分はコメントアウトで無効にしておきます。
          createPage({
            path: edge.node.fields.slug,
            component: slash(postTemplate),
            context: { 
              slug: edge.node.fields.slug,
            },
          })
        */}

          // 以下は、タグ毎、カテゴリ毎のページ作成ですね。デフォルトのままです。

          let tags = []
          if (_.get(edge, 'node.frontmatter.tags')) {
            tags = tags.concat(edge.node.frontmatter.tags)
          }

          tags = _.uniq(tags)
          _.each(tags, tag => {
            const tagPath = `/tags/${_.kebabCase(tag)}/`
            createPage({
              path: tagPath,
              component: tagTemplate,
              context: { tag },
            })
          })

          let categories = []
          if (_.get(edge, 'node.frontmatter.category')) {
            categories = categories.concat(edge.node.frontmatter.category)
          }

          categories = _.uniq(categories)
          _.each(categories, category => {
            const categoryPath = `/categories/${_.kebabCase(category)}/`
            createPage({
              path: categoryPath,
              component: categoryTemplate,
              context: { category },
            })
          })
        }
      })

      resolve()
    })
  })
}

// これ以降もデフォルトのままです。
exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === 'File') {
    const parsedFilePath = path.parse(node.absolutePath)
    const slug = `/${parsedFilePath.dir.split('---')[1]}/`
    createNodeField({ node, name: 'slug', value: slug })
  } else if (
    node.internal.type === 'MarkdownRemark' &&
    typeof node.slug === 'undefined'
  ) {
    const fileNode = getNode(node.parent)
    let slug = fileNode.fields.slug
    if (typeof node.frontmatter.path !== 'undefined') {
      slug = node.frontmatter.path
    }
    createNodeField({
      node,
      name: 'slug',
      value: slug,
    })

    if (node.frontmatter.tags) {
      const tagSlugs = node.frontmatter.tags.map(
        tag => `/tags/${_.kebabCase(tag)}/`
      )
      createNodeField({ node, name: 'tagSlugs', value: tagSlugs })
    }

    if (typeof node.frontmatter.category !== 'undefined') {
      const categorySlug = `/categories/${_.kebabCase(
        node.frontmatter.category
      )}/`
      createNodeField({ node, name: 'categorySlug', value: categorySlug })
    }
  }
}
  • 注意点1: ここのsortはとても大事です。理由は以下のとおり。コード内のconst posts = result.data.allMarkdownRemark.edgesにて、各記事の情報をpostsに格納しますが、その時に全ての記事を時系列に並べるために、sortが必要です。
  • 注意点2: ここで、count変数を作り、記事数をcountへ格納します。gatsbyの公式ページでは、posts.lengthをそのまま記事数としておりますが、これは、postの他にpageが存在しないことが前提です。 例えば、このブログには、記事の他にも、profile, portfolioといったページが存在するので、記事数が30、他のページが3ある場合、posts.lengthで帰ってくる値は33となります。これではページネーションがうまく動作しないので、新たにcount変数を作ることで、記事数のみを取得しております。 gatsbyの公式ページAdding pagination
  • 注意点3: let numPages = Math.ceil(count / postsPerPage)で、先ほど作成したcount変数を用いて、ページ数を取得しています。postsPerPageには6と入っているので、記事数が30の場合には、numPagesは5です。

この時点で、トップページでは、全ての記事ではなくて、最近の6記事のみの見出しが表示されているはずです。/src/pages/index.jsxを無効(削除)にし(もちろん、バックアップを取ることを忘れずに)、gatsby developで確認しましょう。

Terminal
gatsby clean
gatsby develop

ちなにみ、gatsbyのローカル環境に.cacheフォルダや、publicフォルダがあると、変更が正常に反映されないことがあるので、gatsby cleanで、.cacheフォルダ、publicフォルダを消し、gatsby developで開発環境をたちあげましょう。

これで、サイトのトップページから2ページ目、3ページ目へと遷移できるはずです。
問題なく動作できている事を確認できたら、/src/pages/index.jsxは消しましょう。

「新しい記事」、「古い記事」のリンク設定

最後です。各記事のページにおいて、「新しい記事」、「古い記事」というリンクを貼ります。

/src/components/PostTemplateDetails/index.jsx
import React from 'react'
import { Link } from 'gatsby'
import moment from 'moment'
...


class PostTemplateDetails extends React.Component {
  render() {
    const { author } = this.props.data.site.siteMetadata
    const post = this.props.data.markdownRemark
    const htmlAst = post.htmlAst
    const featuredImgSize = post.frontmatter.featuredImage.childImageSharp.sizes.src
    const { tags } = post.frontmatter;
    const path = post.frontmatter;
    const postpath = `https://kinnikumegane.com/${path}`
    const { description: postDescription } = post.frontmatter
    const image = `https://kinnikumegane.com/${featuredImgSize}`

    // ここで、gatsby-node.jsで設定したprevious, nextを読み込みます。
    const { previous, next } = this.props.pageContext
 
    const tagsBlock = (
      <div className="post-single__tags">
        <ul className="post-single__tags-list">
          {tags &&
            tags.map((tag, i) => (
              <li className="post-single__tags-list-item" key={tag}>
                <Link to={tag} className="post-single__tags-list-item-link">
                  {post.frontmatter.tags[i]}
                </Link>
              </li>
            ))}
        </ul>
      </div>
    )

    return (      
      <div>
        <Helmet>
          <meta name="twitter:card" content="summary_large_image" />
          <meta name="twitter:title" content={post.frontmatter.title} />
          <meta name="twitter:image" content={image} />
          <meta name="twitter:description" content={postDescription} />
          <meta name="twitter:site" content="@KinnikuMeganeDe" />
          
          <meta property="og:title" content={post.frontmatter.title} />
          <meta property="og:type" content="article" />
          <meta property="og:image" content={image} />
          <meta property="og:url" content={postpath} />
          <meta property="og:description" content={postDescription} />
        </Helmet>

        <div className="post-single">
          <div className="post-single__inner">
            <h1 className="post-single__title">{post.frontmatter.title}</h1>
            <div className="post-single__date">
              {moment(post.frontmatter.date).format('YYYY年M月DD日')}
            </div>
            <div className="post-single__body">{renderAst(htmlAst)}</div>       
          </div>
          <div className="post-single__footer">
            <h3 className="post-single__footer-sharesuru">シェアする</h3>
            {/* add SNS button*/}
            <a className="twitter-share-button" data-size="large" href="https://twitter.com/share?ref_src=twsrc%5Etfw" data-show-count="false">Tweet</a>

            <ul
              style={{
                display: `flex`,
                flexWrap: `wrap`,
                justifyContent: `space-between`,
                listStyle: `none`,
                padding: 0,
                marginTop: 60,
              }}
            >
              {/* 一番最新の記事であれば、「新しい記事」というリンクは表示しません。 */}
              <li className={next ? '' : 'disabled'}>
                {next&& (
                  <Link to={next.fields.slug} rel="next">
                    ← 新しい記事
                  </Link>
                )}
              </li>
              
              {/* 一番古い記事であれば、「古い記事」というリンクは表示しません。 */}
              <li className={previous ? '' : 'disabled'}>
                {previous && (
                  <Link to={previous.fields.slug} rel="prev">
                    古い記事 →
                  </Link>
                )}
              </li>
                
            </ul>

            <hr />
            ...
          </div>
        </div>
        <Sidebar {...this.props} />
      </div>
    )
  }
}

export default PostTemplateDetails

「新しい記事」、「古い記事」のリンク設定はこれで完了です。

まとめ

ページネーションを追加する上で、以下のページがとても参考になりました。ありがとうございました。

ブログをMiddlemanからGatsbyに乗り換えた雑感
Adding Pagination
Adding a List of Markdown Blog Posts

この記事では、以下の内容について書きました。

  • blog-list-template.jsxを追加する。
  • gatsby-node.jsを変更する。
  • 「新しい記事」、「古い記事」のリンク設定

ページネーションを設定する事で、サイト内でのページ移動が、より簡単になったのではないかと思います。それでは、本日は以上です。

シェアする