Help us understand the problem. What is going on with this article?

Gatsby + ContentfulをTypeScript化する

More than 1 year has passed since last update.

Gatsby + Contentfulという構成にすることで、CMSのフロントエンドとバックエンドを分離することが可能になります。
ただ、この構成にTypeScriptを導入する場合、一手間必要になります。その手順を書いていきます。
また、GatsbyのTypeScript化については、下記の記事が大変参考になりました。内容が被る箇所が多々あるので、その部分についてはこの記事では割愛します。
Gatsby.js を完全TypeScript化する

Gatsbyアプリ作成

まず、gatsby-cliをインストールします。

yarn global add gatsby-cli

その後、下記コマンドを実行することでgatsbyの雛形がダウンロードされます。

gatsby new <任意のディレクトリ名>

続けて以下のコマンドを実行し、localhost:3000でサイトが表示されればOK。

cd <任意のディレクトリ名>
yarn develop

Contenfulのほうもアカウント登録して、適当なSpaceを作成しておきます。

GatsbyのTypeScript化

GatsbyのTypeScript化については、冒頭で紹介したこちらの記事を参考にしてください。自分が書くよりも確実にわかりやすいはず...!

Contentfulの導入

GatsbyとContentfulを繋げるには、プラグインを2つインストールする必要があるので、以下のコマンドを実行します。

yarn add gatsby-source-contentful gatsby-transformer-remark

gatsby-source-contentfulは、ContentfulからGraphQLでデータを取得できるようにするプラグイン。これを導入することでデータ取得用のQueryが利用できるようになります。

gatsby-transformer-remarkは、Markdownで記述されたテキストをHTMLに変換してくれるプラグインです。これを導入することで、Contentful側でMarkdownで書いた内容を、Gatsby側でそのままHTMLとして表示できます。

また、インストールしたプラグインはgatsby-config.jsに記述しないと有効にならないので、忘れないように注意です。

gatsby-config.js
plugins: [
  ...,
  `gatsby-transformer-remark`,
   {
     resolve: `gatsby-source-contentful`,
     options: {
       spaceId: process.env.CONTENTFUL_SPACE_ID,
       accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
     },
   },
]

spaceIdとaccessTokenはContentfulから取得します。envの参照方法はお好みで。自分はdirenvを使いました。

ここまでできたら、Contentful側で適当なコンテンツを作成しておきます。また、GraphiQLで、Contentfulからデータが取得できることを確認しておきましょう。

gatsby-node.jsをTypeScript対応させる

gatsby-node.jsは、主に動的なサイトページを作る際に変更するファイルです(/posts/:idのような)

また、動的なページテンプレートは、src/templates配下に配置します。gatsby-node.jsでContentfulからデータを取得し、そのデータをテンプレートファイルに流し込み、ページを作るイメージです。

今回やりたいのは、gatsby-node.jsで取得したデータ構造を型(Type)としてexportし、テンプレートファイルでそれをimportして紐づけることです。これにより、テンプレート側で型推論を利用したコーディングが可能になります。

参考にした記事に倣って、gatsby-node.jsに以下を記述します。(ts-nodeをインストールしておく必要があります)

gatsby-node.js
'use strict'
require('ts-node').register({
  compilerOptions: {
    module: 'commonjs',
    target: 'esnext',
  },
})
exports.createPages = require('./gatsby-node/index').createPages

これにより、./gatsby-node/index.tsが実質的なエントリーポイントになりました。TypeScriptを用いてgatsby-nodeファイルを記述していくことができます。

ContentfulからPost(投稿)を取得して、動的にページを割り当てるための記述は以下のようになります。

index.ts
const path = require("path")
import { GatsbyNode } from "gatsby"
import {
  ContentfulPostConnection,
  ContentfulPost,
} from "../types/graphql-types"

// GraphQLにより取得されるデータの型
type Result = {
  allContentfulPost: ContentfulPostConnection
}

// テンプレートファイルに渡すデータの型
export type PostContext = {
  post: ContentfulPost
}

// 実行するGraphQLのQuery
const query = `
{
  allContentfulPost {
    edges {
      node {
        content {
          childMarkdownRemark {
            html
          }
          content
        }
        publishedAt
        slug
        title
      }
    }
  }
}
`

// 動的にページを生成する関数
export const createPages: GatsbyNode["createPages"] = async ({
  graphql,
  actions: { createPage },
}) => {
  // ジェネリクスでGraphQLの返却データ型を指定
  const result = await graphql<Result>(query)
  const { edges } = result.data.allContentfulPost

  // 利用するテンプレートファイルを指定
  const postTemplate = path.resolve("./src/templates/post.tsx")

  edges.forEach(edge => {
    // ジェネリクスでcontextプロパティ(テンプレートに渡すデータ)の型を指定
    createPage<PostContext>({
      path: `/posts/${edge.node.slug}`,
      component: postTemplate,
      context: { post: edge.node },
    })
  })
}

ContentfulPostConnectionやContentfulPostといった型は、gatsby-plugin-graphql-codegenというプラグインを導入後、buildすることで自動的に生成されるものです。

GraphQLでContentfulからデータを取得する際のQueryに対応する型が全て自動的に生成されるので、これを用いて型を指定していきます。大量にあるので目当ての型を探すのも一苦労ですが、頑張って探します。

await graphql<Result>(query)で、GraphQLで取得できるデータの型を指定しています。上記のquery(allContentfulPost {...})ではこういったデータが返却されてきます。(GraphiQLでの実行結果です)

{
  "data": {
    "allContentfulPost": {
      "edges": [
        {
          "node": {
            "publishedAt": "2019-11-27T00:00+09:00",
            "slug": "sample",
            "title": "sample",
            "content": {
              "childMarkdownRemark": {
                "html": "<p>sample content</p>"
              }
            }
          }
        }
      ]
    }
  }
}

これに対応する、ContentfulPostConnectionの型はこんな感じ。

export type ContentfulPostConnection = {
  totalCount: Scalars['Int'],
  edges: Array<ContentfulPostEdge>,
  nodes: Array<ContentfulPost>,
  pageInfo: PageInfo,
  distinct: Array<Scalars['String']>,
  group: Array<ContentfulPostGroupConnection>,
};

テンプレートファイルとの型による紐付けは、createPage<PostContext>で行なっています。こう書くことで、以下のcontextの部分をPostContextで縛ることができます。

createPage<PostContext>({
  path: `/posts/${edge.node.slug}`,
  component: postTemplate,
  context: { post: edge.node },
})

contextはテンプレートに渡す値を指定している箇所になるので、ここをPostContextで縛ったことにより、テンプレート側に渡される変数も当然PostContext型ということになります。

post.ts
import React from "react"
import Layout from "../components/layout"
import { PostContext } from "../../gatsby-node"

type Props = {
  pathContext: PostContext
}

// pathContextがgatsby-node.tsから渡される変数で、PostContext型となる
const Post: React.FC<Props> = ({ pathContext }) => {
  const contentHtml = pathContext.post.content.childMarkdownRemark.html

  return (
    <Layout>
      <div dangerouslySetInnerHTML={{ __html: contentHtml }} />
    </Layout>
  )
}

export default Post

これにより、テンプレートファイルでも型推論を利用したコーディングが可能になりますね。

まとめ

自動生成される型定義が多すぎて、目当ての型を探すのに一番苦労しました。ここまでやってしまえば、あとはTypeScriptの恩恵を受けながら開発していけるのかなと思います。

akashixi
29歳未経験からWeb系エンジニアとして働き始めました。拙いながらも積極的にアウトプットしていこうと思います。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away