前の記事で少し触れたように、Contentfulでは自分で設計した Content
に対してTypeScriptの型を自動生成してAPIクライアントに型付けすることができる。この記事ではその方法と、それを利用してコンテンツを取得し、Next.jsでSSGするまでの流れについて書きたいと思う。
何の装飾もない ミニマムなブログサンプルをGitHubに公開しているのでよかったらそちらも参考にしてください。
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"
Contentfulの Setting
から API Keys
を開く。Content management tokens
のタブを選択し、Generate personal token
から token
を作成する。
コピーした token
をフロントエンドプロジェクトの環境変数ファイルにペーストする。自分の場合、プロジェクトに
dotenv
をインストールした上で .env
ファイルを作成し、以下のような形で用意した。
.envCONTENTFUL_SPACE_ID = **** CONTENTFUL_ENVIRONMENT = **** CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN = ****
CONTENTFUL_SPACE_ID
と CONTENTFUL_ENVIRONMENT
は、名前の通りContentfulのスペースIDと環境名である。これらもAPIのページから確認できる。
contentful-typescript-codegen
の README
に倣い、プロジェクトのルートに getContentfulEnvironment.js
として以下のJSファイルを作成する。
getContentfulEnvironment.jsrequire('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
として型定義ファイルが生成されている。
自分はプロジェクトに utils
ディレクトリを作り、そこに client.ts
として定義した。
client.tsimport { 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.localCONTENTFUL_SPACE_ID = **** CONTENTFUL_ENVIRONMENT = **** CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN = **** CONTENTFUL_DELIVERY_TOKEN = **** CONTENTFUL_PREVIEW_TOKEN = ****
ここからの内容は自分がContentfulに用意したコンテツのフィールド名に依存している。ここではブログを作るという想定で、Post
という Content type
を定義したとして進める。なので、コンテンツタイプは post
になっているし、TypeScriptの型は Post
をベースとした名前で生成されている。
SSGによってレンダリングする記事一覧ページのコンポーネントは以下のようになる。
Home.tsxinterface 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を使用することになる。
エラーハンドリングができていないところがまだイマイチだけど、こんなイメージで使用できる。
Post.tsxinterface 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とにらめっこしながらオブジェクトに潜っていく必要はない。