notebook

都内でWEB系エンジニアやってます。

Gatsbyで特定のJSONファイルを用意してGraphQLで扱えるようにする

Gatsbyで作っているブログサイトで、Tagの数が多くなってきたのでTagをまとめたカテゴリ単位の表示をさせたい

JSONファイルに適当なマッピングを持たせてそれを参照させたいというようなケースが発生したのでやってみた

普通にJSONを読み込んでも良いが、せっかくGatsbyで作っているのでJSONからGatsbyのNodeとして扱いGraphQLからクエリできるようにしてみる

今回はgatsby-transformer-jsonを使う

install

yarn add gatsby-transformer-json

設定

const plugins: GatsbyConfig["plugins"] = [
+ `gatsby-transformer-json`,
+  {
+    resolve: `gatsby-source-filesystem`,
+    options: {
+      path: `${__dirname}/content/definitions`,
+      name: `definitions`,
+  },

source-filesystemで特定ディレクトリ以下のファイルを読めるようにした

今回は定義ファイルなのでdefinitionsという名前にした

マッピングファイル

  • content/definitions/categories.json(一部抜粋)
[
  {
    "name": "Cloud",
    "tags": [
      "AWS",
      "GoogleCloud",
      "Cloudflare"
    ]
  },
  {
    "name": "エディタ",
    "tags": ["IntelliJ", "VS Code", "Vim"]
  }
]

とりあえずこの形式にした

ドキュメントや他記事を見る感じ、SchemaをカスタマイズしてGraphQLでタグの文字列とカテゴリを一発取得みたいなことも可能そうに見えたが

ぱっとできそうなイメージが湧かなかったのでまずはマッピングだけを用意し、データ取得するページ側で愚直に回してひもづけるようにした

型定義の生成

これでdevサーバを起動すれば関連する型定義は自動で生成してくれる

あらためて感じたがJSONの中身をみてこんな感じでしょっていうのをしっかり出してきてくれるので良い

Node名はCategoriesJsonになる

これはcategories.jsonというファイル名とtransformer-jsonがJSONを扱うtransformerなのでっぽい(ドキュメントまで調べてない

別のファイルで適当な名前をつけて中身を書いて(hoge.jsonとか)devサーバを起動するとHogeJsonというNodeができてファイルの中身に合わせた型定義が生成される

Tag一覧ページでカテゴリごとに分類する

かなり個別ケースになってしまうが下記のようなコンポーネントを実装した(切り貼りしているのでもしかしたら動かないかも)

import { graphql, PageProps, Link } from "gatsby"
import kebabCase from "lodash/kebabCase"
import React from "react"

type SummarizedTag = {
  [key: string]: {
    count: number
    tags: { fieldValue: string | null; totalCount: number }[]
  }
}

type Tag = {
  fieldValue: string | null
  totalCount: number
}

type TagsWithCountProps = {
  tags: Tag[]
}

const TagsWithCount = ({ tags }: TagsWithCountProps) => {
  return (
    <span className="flex flex-row flex-wrap gap-1">
      {tags.map(tag => (
        <Link key={tag.fieldValue} aria-label="tag" className="label" to={`/tags/${kebabCase(tag?.fieldValue || "")}/`}>
          {tag.fieldValue} ({tag.totalCount})
        </Link>
      ))}
    </span>
  )
}

const TagsPage: React.FC<PageProps<Queries.TagsQuery>> = ({ data }) => {
  const group = data.allMarkdownRemark.group

  const categories = group.reduce((acc, tag) => {
    const category =
      data.allCategoriesJson.edges.find(({ node }) => node?.tags?.includes(tag.fieldValue))?.node.name || "Other"

    const row = acc[category] || { count: 0, tags: [] }
    const newData = { count: row.count + tag.totalCount, tags: [...row.tags, tag] }

    return { ...acc, [category]: { ...newData } }
  }, {} as SummarizedTag)

  return (
    <main className="h-full divide-y divide-zinc-100 bg-white p-4">
      <h1 className="pb-4 text-2xl">Tags</h1>
      {Object.entries(categories)
        .sort((a, b) => (a[0] === "Other" ? 1 : b[1].count - a[1].count))
        .map(([category, row]) => (
          <div key={category} className="py-4">
            <details open>
              <summary key={category}>
                {category} ({row?.count})
              </summary>
              <TagsWithCount tags={row?.tags} />
            </details>
          </div>
        ))}
    </main>
  )
}

export const pageQuery = graphql`
  query Tags {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(limit: 2000) {
      group(field: { frontmatter: { tags: SELECT } }) {
        fieldValue
        totalCount
      }
    }
    allCategoriesJson(limit: 1000) {
      edges {
        node {
          name
          tags
        }
      }
    }
  }
`

export default TagsPage

GraphQLクエリ

  • allMarkdownRemarkのgroupでタグごとの件数を取得
  • allCategoriesJsonでマッピングのリストを取得

集計

上記で取得したデータを元に愚直に集計するだけ

まだいくつか思うところはあるがタグだけが羅列されているよりかはいい感じになった

実装PullRequestは下記

Tagsページの改善 by swfz · Pull Request #1687 · swfz/til