Route360

Gatsby.js + Markdownブログで、カテゴリー機能を実装する方法

目次

ヘッドレスCMSを使っていればカテゴリーやタグの管理は楽ですが、Markdownの場合は一工夫必要です。

英数字のみのカテゴリーであればそこまで難しくありませんが、日本語の場合は「カテゴリー名は日本語、スラッグは英数字」というパターンも多いと思います。

また、SEO対策で、メタタグにそれぞれ工夫したディスクリプションをカテゴリー毎に用意したい場合もあるでしょう。

そういった場合に対応した、カテゴリーの管理方法とカテゴリーページの作り方です。もちろんタグページにも使えます。

動作環境:

  • Node.js v18.12.1
  • React v18.2.0
  • Gatsby.js v5.6.0
  • gatsby-transformer-json v5.6.0

カテゴリー用jsonファイルを作る

今回、カテゴリーはjsonファイルで管理します。ファイルはsrcフォルダー下にdataフォルダーを作り、その中に入れています。

※jsファイルでも可。その場合はgatsby-transformer-jsonは不要。

/src/data/category.json
;[
  {
    title: "コメディー",
    slug: "comedy",
    description:
      "コメディー映画の記事一覧です。コメディーと言えば私の中では三谷幸喜「ラヂオの時間」、当時映画館で爆笑しながら見ました。",
  },
  {
    title: "ホラー",
    slug: "horror",
    description: "ホラー映画の記事一覧です。現実の私はホラー映画は見ませんが。",
  },
  {
    title: "加山雄三",
    slug: "kayamayuzo",
    description:
      "加山雄三さん出演の映画一覧です。永遠の若大将、これからも元気でいてほしいです!",
  },
]

こんな感じで、カテゴリーを作っておきます。もちろん、他のデータを追加してもかまいません。

タグの場合も同様にしてデータを用意して使い回せます。

Markdown記事内での管理

Markdown記事内では、メタデータはYAML Frontmatterを使って管理。その中で、記事が属するカテゴリーとして、カテゴリーのslugを付与します。

/content/posts/funny-10-movies.md
---
title: 腹筋崩壊!涙を流して笑える、コメディー映画10選
slug: funniest-10-movies
category:
  - comedy
date: 2022-10-11
---

本文あれやこれや

上記の例ではカテゴリー「comedy」を指定しています。カテゴリーは複数でも問題ありません。

gatsby-transformer-jsonをインストール

次に、GraphQLでカテゴリーデータを取得するため、公式プラグインのgatsby-transformer-jsonをインストールします。

# npmの場合
npm install gatsby-transformer-json

# yarnの場合
yarn add gatsby-transformer-json

さらに、gatsby-config.jsにプラグインの追加と、gatsby-source-filesystemでjsonファイルのディレクトリを追加しておきます。

/gatsby-config.json
module.exports = {
  //...

  plugins: [
    //...
    `gatsby-transformer-json`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `data`,
        path: `${__dirname}/src/data/`,
      },
    },
  ],
}

これで、カテゴリーデータのクエリCategoryJsonを、GraphQLで取得できるようになります。

Frontmatterのカテゴリーslugと、CategoryJsonのデータを紐付ける

この時点ではまだ、Frontmatterで記事に追加したカテゴリーと、CategoryJsonのデータは紐付いていません。紐付けに使えるのは、CategoryJsonslugですね。

Frontmatterのカテゴリーに指定された文字列と一致するCategoryJsonslugを紐付けて、Frontmatterのカテゴリーからカテゴリーのタイトルなどを取得できるようにします。

gatsby-node.jsにコードを追加。

/gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
  type MarkdownRemark implements Node {
    frontmatter: Frontmatter
  }
  type Frontmatter implements Node {
    category: [CategoryJson] @link(by: "slug")
  }
  `
  createTypes(typeDefs)
}

createSchemaCustomizationは、元来別々のNodeどうしに関連付けを行うためのAPIです。

参考 Create foreign key relationships between data - Creating a Source Plugin | Gatsby

これで、Frontmatterのカテゴリーslugと、category.jsonslugが結びつき、GraphQLで記事スキーマの中にカテゴリー情報が追加されます。

Before: GraphQL
query MyQuery {
  markdownRemark {
    frontmatter {
      category
    }
  }
}
After: GraphQL
query MyQuery {
  markdownRemark {
    frontmatter {
      category {
        id
        title
        slug
      }
    }
  }
}

カテゴリーページのパスを生成

ここからは、カテゴリーページのパス(URL)を作っていきます。gatsby-node.jsの出番です。

ポイントは、クエリ取得にcategoryJsonは使わない点。

というのは、category.jsonの中に用意されたカテゴリーがすべて、必ずしも記事で使われているかはわからないためです。使っていないカテゴリーがある場合、そのカテゴリーページのパスが生成されても困りますよね。

そのため、ここではallMarkdownRemarkから、カテゴリーのグループを利用します。GatsbyのGraphQLは、これがあるから便利なんですよね~😺

参考 Group - GraphQL Query Options | Gatsby

記事のFrontmatterに存在するカテゴリーのslugだけが抽出されるので、「categoryJsonにはあるけれど、実際は記事に使われていないカテゴリー」のパスは生成されません。

これを踏まえて、まずはクエリ追加。

/gatsby-node.js
exports.createPages = async ({ graphql, actions, reporter }) => {
  const { createPage } = actions
  const blogresult = await graphql(`
    query {
      allMarkdownRemark{
        group(field: { frontmatter: { category: SELECT } }) {
          fieldValue
          totalCount
        }
        ...その他色々
      }
    }
  `)

  if (result.errors) {
    reporter.panicOnBuild(`Error while running GraphQL query.`)
    return
  }

  // 以下に続く
}

この後に、createPageでページパスの生成をします。

パス生成時には、コンテキストとしてカテゴリーのスラッグ(ここでは$cat_slug)を用意、テンプレート側で受け取れるようにします。

/gatsby-node.js
// 上記コードの続き

const catPostPerPage = 10 // 1ページあたりの記事数
blogresult.data.allMarkdownRemark.group.forEach(node => {
  const catPosts = node.totalCount
  const catPages = Math.ceil(catPosts / catPostPerPage)
  Array.from({ length: catPages }).forEach((_, i) => {
    createPage({
      path:
        i === 0
          ? `/category/${node.fieldValue}` // 最初のページ
          : `/category/${node.fieldValue}/page/${i + 1}`, // 2ページ目以降
      component: path.resolve(`./src/templates/cat-template.js`), // テンプレート指定
      context: {
        cat_slug: node.fieldValue, // カテゴリースラッグをテンプレートに送る
        skip: catPostPerPage * i,
        limit: catPostPerPage,
      },
    })
  })
})

ここでいったん、ローカルでGatsby.jsを立ち上げて、ブラウザ上で404ページ(存在しないページ)を表示してみましょう。生成されたパスが確認できるはずです。

カテゴリーテンプレートを編集

カテゴリーページのテンプレートでは、GraphQLから2つの階層を利用します。

  • カテゴリー情報であるcategoryJson → カテゴリーのタイトルやディスクリプション
  • 全記事allMarkdownRemark → そのカテゴリーに属する記事すべて

gatsby-node.jsでのカテゴリーページ生成時に、コンテキストとしてcat_slugを作りました。そのcat_slugを使い、表示するカテゴリーや記事一覧の絞り込みをします。

データ取得の例:

/src/templates/cat-template.js
export const query = graphql`
  query ($cat_slug: String!, $skip: Int!, $limit: Int!) {
    allMarkdownRemark(
      filter: {
        frontmatter: { category: { elemMatch: { slug: { eq: $cat_slug } } } }
      }
    ) {
      nodes {
        id
        html
        frontmatter {
          title
          slug
        }
      }
    }
    categoryJson(slug: { eq: $cat_slug }) {
      title
      slug
      description
    }
  }
`

あとはこれを使って、カテゴリーテンプレート内でタイトルを出力するなりすればOK👌!