GatsbyJSでカテゴリー機能を実装する

01 26, 2019

カテゴリーページにもっと自由を

カテゴリー一覧を作っていきたい。 GatsbyのStarterではGraphQLのGroup機能を使ってやっている人が多い。 今回はMarkdownを使ってある程度自由にカスタマイズできるように実装していきたい。
それには理由があって、僕のサイトの作り方だとカテゴリーページが上がる傾向にある。そこでユーザーを離脱させない工夫としてカテゴリーをmarkdownで自由にカスタマイズできるようにしておきたい。

作っていくもの

  • categoriesフォルダ
  • gatsby-node.js
  • /templete/categorypage.js
  • /components/categoryPostlist/

こんだけで実装できそうだ。 まず、/contents/categoies/food.mdと /contents/categories/drink.mdを作成。

---
pagetype: "category"
categoryname: "食べ物"
categoryslug: "food"
---

---
pagetype: "category"
categoryname: "飲み物"
categoryslug: "drink"
---

YAMLで上記のように記載する。 カスタマイズしたければMarkdownで加工すればいいようにする。

gatsby-node.js

続いてgatsby-node.jsを書いていく前にGraphQLでどこにフィルターをかけるかを見てみる。

{
  allMarkdownRemark(filter: {frontmatter: {pagetype: {eq: "category"}}}) {
    edges {
      node {
        id
      }
    }
  }
}

こんな感じでフィルターかけるのが簡単そう。 同じところから引っ張る場合エラーが出るのでそれぞれ名前を付ける

{
  blogposts: allMarkdownRemark(sort: {fields: [frontmatter___date], order: DESC}, limit: 1000) {
    edges {
      node {
        fields {
          slug
        }
        frontmatter {
          title
        }
      }
    }
  }
  categories: allMarkdownRemark(filter: {frontmatter: {pagetype: {eq: "category"}}}) {
    edges {
      node {
        frontmatter {
          categoryslug
        }
      }
    }
  }
}

最終的には

const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

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

  return new Promise((resolve, reject) => {
    const blogPost = path.resolve(`./src/templates/blog-post.js`)
    const categoryPage = path.resolve(`./src/templates/categorypage.js`)
    resolve(
      graphql(
        `
        {
          blogposts: allMarkdownRemark(sort: {fields: [frontmatter___date], order: DESC}, limit: 1000) {
            edges {
              node {
                fields {
                  slug
                }
                frontmatter {
                  title
                }
              }
            }
          }
          categories: allMarkdownRemark(filter: {frontmatter: {pagetype: {eq: "category"}}}) {
            edges {
              node {
                frontmatter {
                  categoryslug
                }
              }
            }
          }
        }
        `
      ).then(result => {
        if (result.errors) {
          console.log(result.errors)
          reject(result.errors)
        }

        // Create blog posts pages.
        const posts = result.data.blogposts.edges
        const categories = result.data.categories.edges

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

          createPage({
            path: post.node.fields.slug,
            component: blogPost,
            context: {
              slug: post.node.fields.slug,
              previous,
              next,
            },
          })
        })

        categories.forEach((category) => {
          createPage({
            path: `/category/${(category.node.frontmatter.categoryslug)}`,
            component: categoryPage,
            context: {
              slug: category.node.frontmatter.categoryslug,
          }})
        })
      })
    )
  })
}

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode })
    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
}

こうなった。 次は、テンプレを作る

import rehypeReact from "rehype-react"
import Sample from "../components/Sample"
import Categoryposts from "../components/categoryposts"

const renderAst = new rehypeReact({
  createElement: React.createElement,
  components: { "sample": Sample }
}).Compiler

class CategoryPageTemplate extends React.Component {
  render() {
    const post = this.props.data.markdownRemark
    const siteTitle = this.props.data.site.siteMetadata.title
    return (
      <Layout location={this.props.location} title={siteTitle}>
        <SEO title={post.frontmatter.categoryname} />
        <h1>{post.frontmatter.categoryname}</h1>
        <div>{renderAst(post.htmlAst)}</div>
        <hr/>
        <Categoryposts category={post.frontmatter.categoryslug} />
      </Layout>
    )
  }
}

export default CategoryPageTemplate

export const pageQuery = graphql`
  query CategoryPageBySlug ($slug: String!){
    site {
      siteMetadata {
        title
        author
      }
    }
    markdownRemark (frontmatter:{categoryslug:{ eq: $slug}})
    {
        htmlAst
        frontmatter {
        categoryname
        categoryslug
      }
    }
  }
`

最終的にこうなった。 そして、categorylistを作る。 /src/components/categoryposts/index.js

import React from 'react'
import { StaticQuery, graphql } from 'gatsby'

const CategoryPosts = (props) => (
  <StaticQuery
    query={graphql`
    query{
        allMarkdownRemark{
          edges{
            node{
              frontmatter{
                pagetype
                category
                title
                date
              }
              excerpt
            }
          }
        }
      }
    `}

    render={(data) => {
      const postlists = data.allMarkdownRemark.edges
      const posts = postlists.filter((category)=>{
          return (category.node.frontmatter.category === props.category)
      })

      return (
      <div>
      {posts.map(({ node }) => {
          return (
            <div key={node.frontmatter.title}>
              <p>{node.frontmatter.date}</p>
              <p dangerouslySetInnerHTML={{ __html: node.excerpt }} />
            </div>
          )
        })}
      </div>
      );
    }}
  />
)
export default CategoryPosts

ポイントはStaticQueryを使って全部取得してからフィルタリングしているところ。 コンポーネントでGraphQLを使用する場合、普通のクエリではと動かない仕様になっている。 gatsby 上手く表示できた!


コリ

コリといいます。奈良県でサラリーマンをしています。GatsbyJSでサイトを作るのが趣味です。