Contentfulで型安全にコンテンツを取得する
前の記事で少し触れたように、Contentfulでは自分で設計した Content に対してTypeScriptの型を自動生成してAPIクライアントに型付けすることができる。この記事ではその方法と、それを利用してコンテンツを取得し、Next.jsでSSGするまでの流れについて書きたいと思う。
何の装飾もないミニマムなブログサンプルをGitHubに公開しているのでよかったらそちらも参考にしてください。
TypeScriptの型を生成する
必要なパッケージをインストールする
contentfulcontentful-managementcontentful-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 ファイルを作成し、以下のような形で用意した。
.envCONTENTFUL_SPACE_ID = **** CONTENTFUL_ENVIRONMENT = **** CONTENTFUL_MANAGEMENT_API_ACCESS_TOKEN = ****
CONTENTFUL_SPACE_ID と CONTENTFUL_ENVIRONMENT は、名前の通りContentfulのスペースIDと環境名である。これらもAPIのページから確認できる。
TypeScriptの型生成
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 として型定義ファイルが生成されている。
APIクライアントを定義する
自分はプロジェクトに 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 = ****
Next.jsでSSGする
ここからの内容は自分が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を使用することになる。
エラーハンドリングができていないところがまだイマイチだけど、こんなイメージで使用できる。
1件の記事の取得
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とにらめっこしながらオブジェクトに潜っていく必要はない。