Contentfulで型安全にコンテンツを取得する

投稿日: March 1, 2021

前の記事で少し触れたように、Contentfulでは自分で設計した Content に対してTypeScriptの型を自動生成してAPIクライアントに型付けすることができる。この記事ではその方法と、それを利用してコンテンツを取得し、Next.jsでSSGするまでの流れについて書きたいと思う。

何の装飾もない ミニマムなブログサンプルをGitHubに公開しているのでよかったらそちらも参考にしてください。

TypeScriptの型を生成する

必要なパッケージをインストールする

  • contentful
  • contentful-management
  • contentful-typescript-codegen
  • @contentful/rich-text-types

の4つのパッケージをインストールする。ここでは npm を利用する。

npm i contentful contentful-management contentful-typescript-codegen @contentful/rich-text-types

package.json に次のnpmスクリプトを定義する。

"contentful-typescript-codegen": "contentful-typescript-codegen --output @types/generated/contentful.d.ts"

Management APIのtokenを取得する

Contentfulの Setting から API Keys を開く。Content management tokens のタブを選択し、Generate personal token から token を作成する。

コピーした token をフロントエンドプロジェクトの環境変数ファイルにペーストする。自分の場合、プロジェクトに dotenv をインストールした上で .env ファイルを作成し、以下のような形で用意した。

.env
CONTENTFUL_SPACE_ID = **** CONTENTFUL_ENVIRONMENT = **** CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN = ****

CONTENTFUL_SPACE_IDCONTENTFUL_ENVIRONMENT は、名前の通りContentfulのスペースIDと環境名である。これらもAPIのページから確認できる。

TypeScriptの型生成

contentful-typescript-codegenREADME に倣い、プロジェクトのルートに getContentfulEnvironment.js として以下のJSファイルを作成する。

getContentfulEnvironment.js
require('dotenv').config(); const contentfulManagement = require('contentful-management'); module.exports = function () { const contentfulClient = contentfulManagement.createClient({ accessToken: process.env.CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN, }); return contentfulClient .getSpace(process.env.CONTENTFUL_SPACE_ID) .then((space) => space.getEnvironment(process.env.CONTENTFUL_ENVIRONMENT)); };

保存したら、npm run contentful-typescript-codegen を実行する。成功すれば @types/generated 下に contentful.d.ts として型定義ファイルが生成されている。

APIクライアントを定義する

自分はプロジェクトに utils ディレクトリを作り、そこに client.ts として定義した。

client.ts
import { createClient } from 'contentful'; // contentful の APIクライアント const client = createClient({ accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN || '', space: process.env.CONTENTFUL_SPACE_ID || '', }); export default client;

上のAPIクライアントではいくつかの環境変数を参照している。Next.jsの場合、.env.local ファイルを作成し、以下のように用意しておけば良い。ここでも中身はContentfulのAPIページからtokenをコピペしておく。

.env.local
CONTENTFUL_SPACE_ID = **** CONTENTFUL_ENVIRONMENT = **** CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN = **** CONTENTFUL_DELIVERY_TOKEN = **** CONTENTFUL_PREVIEW_TOKEN = ****

Next.jsでSSGする

ここからの内容は自分がContentfulに用意したコンテツのフィールド名に依存している。ここではブログを作るという想定で、Post という Content type を定義したとして進める。なので、コンテンツタイプは post になっているし、TypeScriptの型は Post をベースとした名前で生成されている。

記事一覧の取得

SSGによってレンダリングする記事一覧ページのコンポーネントは以下のようになる。

Home.tsx
interface Props { posts: EntryCollection<IPostFields>; }; const Home: React.FC<Props> = ({ posts }) => ( // 省略 ) export default Home; // Next.js の SSG のためのコード export const getStaticProps: GetStaticProps<Props> = async () => { const posts = await client .getEntries<IPostFields>({ content_type: 'post', }) .then((result) => { return result; }); return { props: { posts, }, }; };

一覧の取得はAPIクライアントで getEntries() というメソッドを利用する。引数にコンテンツタイプ(ここでは 'post')を指定することで、目的の種類のコンテンツだけを取得することができる。

Contentfulが返すコンテンツは、自分が定義したコンテツが入る fields プロパティの他に、sys というプロパティで、投稿日やIDなどシステム側で設定された値を持っている。別コンテンツへリンクを作成する際にはこのIDを使用することになる。

エラーハンドリングができていないところがまだイマイチだけど、こんなイメージで使用できる。

1件の記事の取得

Post.tsx
interface Props { post: Entry<IPostFields>; } const Post: React.FC<Props> = ({ post }) => ( // 省略 ) export default Post; interface Params extends ParsedUrlQuery { id: string; } export const getStaticPaths: GetStaticPaths = async () => { const posts = await client.getEntries<IPostFields>().then((result) => { return result; }); const paths = posts.items.map((item) => `/blog/${item.sys.id}`); return { paths, fallback: false }; }; export const getStaticProps: GetStaticProps<Props, Params> = async ( context ) => { const id = context.params?.id || ''; const post = await client.getEntry<IPostFields>(id).then((result) => { return result; }); return { props: { post, }, }; };

ビルド時にページのパスを決定するための getStaticPaths と、Porpsを渡すための getStaticProps を定義している。こちらもエラーハンドリングができていなかったり、IDが undefined のときに空文字を充てているなど、まだまだイケてないところがある。

さいごに

Contentfulは柔軟にコンテンツを構成できる一方で、APIから帰ってくるオブジェクトも複雑なネスト構造になりやすい。しかし、型があることでそんなネスト構造は知らなくとも、サジェストに従っていけば迷わず目的のコンテンツを参照することができる。コンテンツ構造を変えたときにも、型を自動生成すれば修正も楽である。JSONとにらめっこしながらオブジェクトに潜っていく必要はない。


share on...