🍇

GatsbyJS + TypeScript 環境構築 2022

2022/05/26に公開
2

この記事は、ジャムジャム!!Jamstack_7 で発表したLT
「2022年最新版GatsbyJS+TypeScript+microCMSでブログを作る。」を元にした記事です。

GatsbyJSってなに?

GatsbyJSは、
静的サイトジェネレーターでReactをベースに開発されているフレームワークです。
特徴として、Gatsbyではデータを取得する際にはGraphQLを利用します。
GraphQLを使って内部リソースのMarkdownファイルや、CMSなどのさまざまなデータを取得することができます。

今回扱う主な依存関係

依存関係名 バージョン
GatsbyJS 4.15.0
TypeScript 4.6.4
React 18.1.0

GatsbyJSの環境構築

公式のQuick startの従って下記のコマンドを実行します。

npm init gatsby

https://www.gatsbyjs.com/docs/quick-start/

対話式でいくつか質問されるので、適切に答えていきます。
質問内容をGoogle翻訳にかけましたので、参考になればと思います。

What would you like to call your site?
(あなたのサイトを何と呼びたいですか?)
✔ · Example GatsbyJS 2022

What would you like to name the folder where your site will be created?
(サイトが作成されるフォルダにどのような名前を付けますか?)
✔ atelier/ example-gatsbyjs-2022

✔ Will you be using JavaScript or TypeScript?
(JavaScriptまたはTypeScriptを使用しますか?)
· TypeScript

✔ Will you be using a CMS?
(CMSを使用しますか?)
· No (or I'll add it later)

✔ Would you like to install a styling system?
(スタイリングシステムをインストールしますか?)
· Emotion

✔ Would you like to install additional features with other plugins?
(他のプラグインで追加機能をインストールしますか?)
· Add responsive images
· Add page meta tags with React Helmet
· Add an automatic sitemap
· Generate a manifest file

Thanks! Here's what we'll now do:

    🛠  Create a new Gatsby site in the folder example-gatsbyjs-2022
    🎨 Get you set up to use Emotion for styling your site
    🔌 Install gatsby-plugin-image, gatsby-plugin-react-helmet, gatsby-plugin-sitemap, gatsby-plugin-manifest

✔ Shall we do this? (Y/n)

この方法で生成されるプロジェクトは、gatsby-starter-minimal-tsというスターターリポジトリです。
https://github.com/gatsbyjs/gatsby-starter-minimal-ts

GraphQL Typegen を適応する

GatsbyJSのv4.15で投入された GraphQL Typegen を利用します。
https://www.gatsbyjs.com/docs/reference/release-notes/v4.15/

gatsby-config.ts
const config: GatsbyConfig = {
    // ...
    graphqlTypegen: true
}

これにより、ローカルサーバーが立ち上がっている場合は、動的に src/gatsby-types.d.ts にGraphQLの型定義が生成されます。

詳しい設定方法などは、こちらをご覧ください。
https://www.gatsbyjs.com/docs/how-to/local-development/graphql-typegen/

GraphQLのクエリを作成する際に補完を効かせる。

VSCodeの GraphQL Plugin を利用して補完機能を利用する場合は下記のフォルダを作成します。

graphql.config.js
module.exports = require("./.cache/typegen/graphql.config.json")

これを実施することでクエリを記述する際に補完が効くようになり、下記のように補完されるようになります。

ここまでで、GatsbyJSのメインの環境構築は完了したかと思います。
このあとは、headlessCMSとの繋ぎ込みを少しご紹介します。

headlessCMSと繋ぎこむ

今回は(いつも)、microCMSをheadlessCMSとして繋ぎ込みます。

APIは事前にテンプレートのブログを選択して作成しておきます。
https://blog.microcms.io/api-template/

gatsby-source-microcmsを追加する

microCMSが公式で提供しているGatsbyJS plugin、gatsby-source-microcmsを利用します。
https://www.gatsbyjs.com/plugins/gatsby-source-microcms/

npm install gatsby-source-microcms
gatsby-config.ts
import type { GatsbyConfig } from "gatsby";

requier('dotenv').config()

const config: GatsbyConfig = {
  plugins: [
    // ...,
    {
      resolve: 'gatsby-source-microcms',
      options: {
        apiKey: process.env.MICROCMS_API_KEY,
        serviceId: process.env.MICROCMS_SERVICE_ID,
        apis: [
    	  {
	    endpoint: 'blogs',
	  },
	  {
	    endpoint: 'categories',
	  },
        ],
      },
    },
    // ...
  ]
}
.env
MICROCMS_SERVICE_ID=< microCMSのサービスID >
MICROCMS_API_KEY=< microCMSのAPIキー >

gatsby-plugin-microcmsの設定を入力すればあとはGraphQLから取得するだけです。

pages/index.tsxにGraphQLでの情報の取得と取得した情報の表示をしてみましょう。

pages/index.tsx
import { graphql, Link, PageProps } from 'gatsby'
import React from 'react'
import { formatDate } from '../utils/date'

export default function IndexPage({ data }: PageProps<Queries.IndexPageQuery>) {
  const { allMicrocmsBlogs } = data
  return (
    <main>
      <h1>TOPページ</h1>
      <p>このページはGatsbyで作成されています。</p>
      <h2>最新記事</h2>
      <ul>
        {allMicrocmsBlogs.nodes.map(node => (
          <li key={node.blogsId}>
            <Link to={`/blogs/${node.blogsId}/`}>
            {node.title}【公開日:{formatDate(node.publishedAt!)}</Link>
          </li>
        ))}
      </ul>
      <Link to="/blogs/">もっとみる</Link>
    </main>
  )
}

export const query = graphql`
  query IndexPage {
    allMicrocmsBlogs(limit: 3, sort: { order: DESC, fields: publishedAt }) {
      nodes {
        blogsId
        title
        publishedAt
        revisedAt
      }
    }
  }
`
utils/date.ts
import dayjs from 'dayjs'

export const formatDate = (date: string) => {
  return dayjs(date).format('YYYY-MM-DD')
}

上記のコードで、最新3件のブログ情報を取得、表示ができたかと思います。

ブログの詳細ページと一覧ページを作成する。

動的なページは、gatsby-node.tsにページを作成する処理を記載します。

一覧ページは、/blogs/, /blogs/page/2...のように、
詳細ページは、/blog/{contentId}になるようにページを作成していきます。

詳細ページを生成する。

ページを生成する際にはgatsby-node.tsにて、createPages関数を named exportする必要があります。createPagesはgatsbyのライフサイクルによって実行タイミングがコントロールされています。

今回はそのライフサイクル内で、ページを作成するための関数。createPageを用いてページを作成していきます。

https://www.gatsbyjs.com/docs/reference/config-files/actions/#createPage

gatsby-node.ts
import { GatsbyNode } from 'gatsby'
import path from 'path'

export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions: { createPage } }) => {
  const result = await graphql<Queries.CreatePagesQuery>(`
    query CreatePages {
      allMicrocmsBlogs(sort: { order: ASC, fields: publishedAt }) {
        edges {
          node {
            blogsId
          }
          next {
            blogsId
            title
          }
          previous {
            blogsId
            title
          }
        }
      }
    }
  `)

  if (result.errors) {
    throw result.errors
  }

  const { allMicrocmsBlogs } = result.data!

  allMicrocmsBlogs.edges.forEach((edge) => {
    createPage({
      // 生成したいページのpathを記載します。
      path: `/blog/${edge.node.blogsId}/`,
      // ページの土台となるコンポーネントのパスを記述します。
      component: path.resolve('src/templates/blog.tsx'),
      // コンポーネントの`pageContext`という引数情報に渡すデータを格納します。
      // pageContextに渡した値は、templateのGraphQL内で変数として利用できます。
      context: {
        id: edge.node.blogsId,
        next: edge.next,
        previous: edge.previous
      }
    })
  })
}

allMicrocmsBlogsで取得した全ブログコンテンツをforEachを用いて順々にページを作成しています。
又、GraphQLでedgesを用いることで、前の記事・次の記事の情報も併せて取得することができます。

templates/blog.tsx
import { graphql, Link, PageProps } from 'gatsby'
import React from 'react'

type PageContext = {
  next: {
    blogsId: string
    title: string
  } | null
  previous: {
    blogsId: string
    title: string
  } | null
}

export default function BlogPage({
  data,
  pageContext: { next, previous },
  location
}: PageProps<Queries.BlogPageQuery, PageContext>) {
  const { microcmsBlogs } = data
  return (
    <main>
      <h1>{microcmsBlogs?.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: microcmsBlogs?.content ?? '' }} />
      <ul>
        {next && (
          <li>
            次へ:
            <Link to={`/blogs/${next.blogsId}/`}>{next.title}</Link>
          </li>
        )}
        {previous && (
          <li>
            前へ:
            <Link to={`/blogs/${previous.blogsId}/`}>{previous.title}</Link>
          </li>
        )}
      </ul>
    </main>
  )
}

export const query = graphql`
  query BlogPage($id: String!) {
    microcmsBlogs(blogsId: { eq: $id }) {
      blogsId
      title
      content
      publishedAt
    }
  }
`

pageContextで渡ってきた情報を$id: String!で利用します。

export const query = graphql`
  query BlogPage($id: String!) {
    microcmsBlogs(blogsId: { eq: $id }) {
      blogsId
      title
      content
      publishedAt
    }
  }
`

こちらの記述は、$idを用いてblogsIdが一致するコンテンツ情報を取得するというクエリです。
こうすることで、gatsby-node.tsには最小限のクエリを記載して、ページで扱う詳細な情報のクエリの記述位置とをファイルの責務分担ができます。

一覧ページを生成する。

次に、一覧ページを作成します。

一覧ページは、/blogs/, /blogs/page/2...のように、

前述した条件でページを生成していきます。

今回は、all*が持っているtotalCountを用いてページを生成していきます。

また、ベースコンポーネントに渡すcontexを作成するutils/page.tsも今回は作成しました。

utils/page.ts
type GetPageContextsParams = {
  totalCount: number
  limit: number
}

export const getPagesContext = ({ totalCount, limit }: GetPageContextsParams) => {
  const totalPagesCount = Math.ceil(totalCount / limit)

  return new Array(totalPagesCount).fill('').map((_, i) => {
    const offset = limit * i
    const currentPageNum = ((offset + limit) / limit)

    return {
      limit, // 1ページに表示する件数
      offset, // 何番目のコンテンツから表示させるか
      totalCount, // コンテンツの総量
      currentPageNum, // 現在のページ番号
      totalPagesCount // ページの総量
    } as const
  })
}

getPagesContextの内容は、一覧ページを構成する上であると良い情報を算出して配列として返却する関数となっています。

gatsby-node.ts
import { GatsbyNode } from 'gatsby'
import path from 'path'
import { getPagesContext } from './src/utils/pages'

export const createPages: GatsbyNode['createPages'] = async ({ graphql, actions: { createPage } }) => {
  const result = await graphql<Queries.CreatePagesQuery>(`
    query CreatePages {
      allMicrocmsBlogs(sort: { order: ASC, fields: publishedAt }) {
        // 中略
        totalCount
      }
    }
  `)

  if (result.errors) {
    throw result.errors
  }

  const { allMicrocmsBlogs: { totalCount, edges } } = result.data!

  // 詳細ページ中略...

  const pagesContext = getPagesContext({
    totalCount,
    limit: 10 // 1ページあたり10コンテンツを表示させる
  })

  pagesContext.forEach((context) => {
    const component = path.resolve('src/templates/blogs.tsx')

    if (context.currentPageNum === 1) {
      createPage({
        path: `/blogs/`,
        component,
        context
      })
      return
    }

    createPage({
      path: `/blogs/page/${context.currentPageNum}/`,
      component,
      context
    })
  })
}

getPagesContextで取得した情報を元にforEachで順々にページを構成していきます。

templates/blogs.tsx
import { graphql, PageProps, navigate, Link } from 'gatsby'
import React from 'react'
import { formatDate } from '../utils/date'

type PageContext = {
  limit: number
  offset: number
  totalCount: number
  currentPageNum: number
  totalPagesCount: number
}

export default function BlogsPage({
  data,
  pageContext: { limit, offset, totalCount, currentPageNum, totalPagesCount },
  location
}: PageProps<Queries.BlogsPageQuery, PageContext>) {
  const { allMicrocmsBlogs } = data
  return (
    <main>
      <h1>ブログ一覧</h1>
      <p>{totalCount} 件中 {offset + 1} 件目から {limit} 件表示</p>
      <ul>
        {allMicrocmsBlogs.nodes.map((node) => (
          <li key={node.blogsId}>
            <Link to={`/blogs/${node.blogsId}/`}>
            {node.title}【公開日:{formatDate(node.publishedAt!)}</Link>
          </li>
        ))}
      </ul>

      <select
        onChange={(e) => {
          navigate(e.target.value === '1' ? '/blogs/' : `/blogs/page/${e.target.value}/`)
        }}
      >
        {new Array(totalPagesCount).fill('').map((_, i) => {
          const pageNum = i + 1
          return (
            <option value={pageNum} key={i} selected={pageNum === currentPageNum}>
              {pageNum} ページ目
            </option>
          )
        })}
      </select>
    </main>
  )
}

export const query = graphql`
  query BlogsPage($limit: Int!, $offset: Int!) {
    allMicrocmsBlogs(limit: $limit, skip: $offset, sort: { order: DESC, fields: publishedAt }) {
      nodes {
        blogsId
        title
        publishedAt
        revisedAt
      }
    }
  }
`

context経由で渡ってきている limit** と **offset を用いて一覧に表示するコンテンツを取得します。内容は、公開日降順で○番目(offset)から始まるコンテンツ最大10件(limit)を取得する。
という内容になっています。

取得した情報を用いて、詳細ページに遷移させるitemを生成していきます。

ここまでで、大まかなGatsbyJSの利用方法解説を終えます。

最後に

GatsbyJSも新しいバージョンになってきてTypeScriptでのコーディングが日にひにやりやすくなってきていて、RFCなどでもあったGraphQLTypeGenなどの登場でますます開発体験が向上していくのを感じています。

GraphQLをふれたことのない方だと、抵抗感があるかもしれません。(私はそうでした)
しかし、Next.jsに負けず劣らず、GatsbyJSを採用する方が良い場面というのも存在していると私は感じています。

これからもGatsbyJSを業務や私用で使っていきたいなぁ〜と思います。
是非、興味を持たれた方は一度触ってみてください!

おまけ

Q. Next.jsでもおんなじことできますよね?

A. できます。

むしろNext.jsISRを利用したいパターンが多くあるので私もNext.jsを採用する機会の方が多いです。

その中でも私がGatsbyJSを推していく理由の一つが、公式やコミュニティによるプラグインの豊富さと開発に至るまでの速度感です。
Markdownファイルや画像の最適化、各種CMSからのデータ取得などが、pluginを読み込むことでおおよそのデータが開発環境で扱えるようになります。

ページ数が少なく、ISRを利用する必要がないケースなどでSEOを気にしたサイトを製作するにはうってつけのフレームワークであると思います。

Discussion

あかれぎあかれぎ

GatsbyJS の GraphQL 型生成の設定が gatsby@4.15.0 で変わったようです。

gatsby-config.ts
const config: GatsbyConfig = {
    // ...
    graphqlTypegen: true
}

参考: https://www.gatsbyjs.com/docs/how-to/local-development/graphql-typegen/

現在 npm init gatsby を実行すると 4.15 が入るため、flags: { ... } を記述すると警告が出ます。

hanetsukihanetsuki

ありがとうございます!configの設定として採用されたんですね...
こちら記事の内容アップデートさせていただいきます!

有益な情報ありがとうございますー🙏