GatsbyJS + TypeScript 環境構築 2022
この記事は、ジャムジャム!!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
対話式でいくつか質問されるので、適切に答えていきます。
質問内容を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というスターターリポジトリです。
GraphQL Typegen を適応する
GatsbyJSのv4.15
で投入された GraphQL Typegen を利用します。
const config: GatsbyConfig = {
// ...
graphqlTypegen: true
}
これにより、ローカルサーバーが立ち上がっている場合は、動的に src/gatsby-types.d.ts
にGraphQLの型定義が生成されます。
詳しい設定方法などは、こちらをご覧ください。
GraphQLのクエリを作成する際に補完を効かせる。
VSCodeの GraphQL Plugin を利用して補完機能を利用する場合は下記のフォルダを作成します。
module.exports = require("./.cache/typegen/graphql.config.json")
これを実施することでクエリを記述する際に補完が効くようになり、下記のように補完されるようになります。
ここまでで、GatsbyJSのメインの環境構築は完了したかと思います。
このあとは、headlessCMSとの繋ぎ込みを少しご紹介します。
headlessCMSと繋ぎこむ
今回は(いつも)、microCMSをheadlessCMSとして繋ぎ込みます。
APIは事前にテンプレートのブログを選択して作成しておきます。
gatsby-source-microcmsを追加する
microCMSが公式で提供しているGatsbyJS plugin、gatsby-source-microcms
を利用します。
npm install gatsby-source-microcms
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',
},
],
},
},
// ...
]
}
MICROCMS_SERVICE_ID=< microCMSのサービスID >
MICROCMS_API_KEY=< microCMSのAPIキー >
gatsby-plugin-microcms
の設定を入力すればあとはGraphQLから取得するだけです。
pages/index.tsxにGraphQLでの情報の取得と取得した情報の表示をしてみましょう。
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
}
}
}
`
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を用いてページを作成していきます。
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
を用いることで、前の記事・次の記事の情報も併せて取得することができます。
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
も今回は作成しました。
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の内容は、一覧ページを構成する上であると良い情報を算出して配列として返却する関数となっています。
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
で順々にページを構成していきます。
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
経由で渡ってきている
という内容になっています。
取得した情報を用いて、詳細ページに遷移させるitemを生成していきます。
ここまでで、大まかなGatsbyJSの利用方法解説を終えます。
最後に
GatsbyJSも新しいバージョンになってきてTypeScriptでのコーディングが日にひにやりやすくなってきていて、RFCなどでもあったGraphQLTypeGenなどの登場でますます開発体験が向上していくのを感じています。
GraphQLをふれたことのない方だと、抵抗感があるかもしれません。(私はそうでした)
しかし、Next.jsに負けず劣らず、GatsbyJSを採用する方が良い場面というのも存在していると私は感じています。
これからもGatsbyJSを業務や私用で使っていきたいなぁ〜と思います。
是非、興味を持たれた方は一度触ってみてください!
おまけ
Q. Next.jsでもおんなじことできますよね?
A. できます。
むしろNext.jsのISR
を利用したいパターンが多くあるので私もNext.jsを採用する機会の方が多いです。
その中でも私がGatsbyJSを推していく理由の一つが、公式やコミュニティによるプラグインの豊富さと開発に至るまでの速度感です。
Markdownファイルや画像の最適化、各種CMSからのデータ取得などが、pluginを読み込むことでおおよそのデータが開発環境で扱えるようになります。
ページ数が少なく、ISRを利用する必要がないケースなどでSEOを気にしたサイトを製作するにはうってつけのフレームワークであると思います。
Discussion
GatsbyJS の GraphQL 型生成の設定が
gatsby@4.15.0
で変わったようです。参考: https://www.gatsbyjs.com/docs/how-to/local-development/graphql-typegen/
現在
npm init gatsby
を実行すると 4.15 が入るため、flags: { ... }
を記述すると警告が出ます。ありがとうございます!configの設定として採用されたんですね...
こちら記事の内容アップデートさせていただいきます!
有益な情報ありがとうございますー🙏