💻

GatsbyJSで作っているブログにタグ機能を導入した

2019-12-01

はじめに

ギフティ Advent Calendar 2019 - Qiitaの1日目の記事。このブログにタグ機能を導入したので、その時の手順を解説する。基本的に公式の記事に従えば良いのだが、差分が分かりづらいのと、自分で理解した内容を補足したいので、記事にした。

前提

このブログはGatsby公式のgatsby-starter-blogをベースに作られている。

gatsbyjs/gatsby-starter-blog: Gatsby starter for creating a blog

デモ

この記事に付けられているタグ(「GatsbyJSで最強のブログを作る」)をクリックすると、そのタグが付けられた記事に絞り込まれた一覧を確認できる。

手順

  • 各記事に tags を追加する
  • 各タグの一覧ページが生成されるようにする
  • テンプレートを作成する
  • 記事ページにタグを載せる

各記事に tags を追加する

各記事のマークダウンファイルに下記のようにtagsを追加していく。

index.md
---
title: "GatsbyJSで作っているブログにタグ機能を導入した"
date: "2019-12-01 22:00"
tags: ["GatsbyJSで最強のブログを作る", "GatsbyJS"]---

## はじめに

この作業が終わった時点で、 http://localhost:8000/___graphql に行き、

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

というクエリを投げると、

結果
{
  "data": {
    "allMarkdownRemark": {
      "group": [
        {
          "tag": "CSS",
          "totalCount": 2
        },
        {
          "tag": "GatsbyJS",
          "totalCount": 3
        },
        {
          "tag": "Netlify",
          "totalCount": 1
        }
        ...
      ]
    }
  }
}

このような結果が得られる。タグ毎にグルーピングされた結果が取得できていることが分かる。このタグのグループを取得するクエリを上手く活用して、各タグの記事一覧ページを生成していく。

各タグの記事一覧ページが生成されるようにする

このブログで言う、下記のようなページだ。

https://kikunantoka.com/tags/gatsby-js/

各マークダウンファイルのtagsの値を読み込んで、tags/{tags} に各タグの記事一覧ページが生成されるように gatsby-node.js ファイルを下記のように編集していく。ハイライトしている部分に注目して欲しい。

gatsby-node.js
const path = require(`path`)
const _ = require("lodash")const { createFilePath } = require(`gatsby-source-filesystem`)

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions
  const postTemplate = path.resolve(`./src/templates/posts.js`)
  const tagTemplate = path.resolve(`./src/templates/tags.js`)
  return graphql(
    `
      {
        posts: allMarkdownRemark(
          sort: { fields: [frontmatter___date], order: DESC }
          limit: 1000
        ) {
          edges {
            node {
              fields {
                slug
              }
              frontmatter {
                title
              }
            }
          }
        }
        tags: allMarkdownRemark(limit: 1000) {          group(field: frontmatter___tags) {            fieldValue          }        }      }
    `
  ).then(result => {
    if (result.errors) {
      throw result.errors
    }

    const posts = result.data.posts.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: postTemplate,
        context: {
          slug: post.node.fields.slug,
          previous,
          next,
        },
      })
    })

    const tags = result.data.tags.group    tags.forEach(tag => {      createPage({        path: `/tags/${_.kebabCase(tag.fieldValue)}/`,        component: tagTemplate,        context: {          tag: tag.fieldValue,        },      })    })
    return null
  })
}

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

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

先ほど試したクエリを使って、タグ一覧を取ってきて、それらのタグ毎にテンプレートに当てはめたページを生成していく内容となっている。createPageのcomponentにタグ用のテンプレートを渡している点と、contextにそのタグ名を渡している点が注目点だ。

テンプレートを作成する

各タグの記事一覧ページ用のテンプレートを作る。先ほどの gatsby-node.js で読み込んだパスと同じであれば、どこに配置しても良い。src/templates/ のようなフォルダを切ると良さそう。下記がこのブログの各タグの記事一覧ページ用のテンプレートだ。

src/templates/tags.js
import React from "react"

import Bio from "../components/bio"
import Layout from "../components/layout"
import Section from "../components/section"
import Post from "../components/post"

const Tags = ({ pageContext, data }) => {
  return (
    <Layout>
      <Section>
        <Bio />
      </Section>
      <h1>
        {pageContext.tag} ({data.allMarkdownRemark.totalCount})
      </h1>
      {data.allMarkdownRemark.edges.map(({ node }) => {
        return (
          <Post
            slug={node.fields.slug}
            title={node.frontmatter.title}
            date={node.frontmatter.date}
            tags={node.frontmatter.tags}
          />
        )
      })}
    </Layout>
  )
}

export default Tags
export const pageQuery = graphql`
  query($tag: String) {    allMarkdownRemark(      limit: 1000      sort: { fields: [frontmatter___date], order: DESC }      filter: { frontmatter: { tags: { in: [$tag] } } }    ) {      totalCount      edges {        node {          excerpt(truncate: true)          fields {            slug          }          frontmatter {            title            date            tags          }        }      }    }  }

createPageのcontextに渡したtagの内容を元にフィルタリングした記事一覧を取得して、その記事一覧を描画する内容になっている。記事のパラメータを渡すと記事へのリンクと概要を表示してくれるPostコンポーネントのようなものを用意しておくと、記事一覧ページとタグページで共通化ができて良い。

記事ページにタグを載せる

各タグへのリンクは、下記のサンプルのように gatsby-node.js で記述したcreatePageのpathにリンクすると良い。こちらでも同様にlodashを活用する。

サンプル
import _ from "lodash"

{tags.map(tag => {
  return <Link to={`/tags/${_.kebabCase(tag)}/`}>{tag}</Link>
})}

記事ページ側で、titleやdateと同様にtagsも取得するようにクエリを修正した上で、上述のようなリンクを追加すれば、各記事にタグが表示され、そのタグをクリックするとクリックしたタグの記事一覧ページが表示されるようになる。あとはスタイルを整えれば、完成だ。以上で作業は終了となる。

さいごに

記事を書こうとして、理解が深まったり、リファクタが進んだので良かった。

キクナントカ

キクナントカ

ソフトウェアエンジニアをしています。Flutter、Rails、GatsbyJSを主に触っています。趣味でボードゲームを制作したり、個人開発したりしています。詳しくはこちら