ContentfulとAstro
Contentfulはコンテンツの管理と他サービスとの連携、マルチプラットフォームへの公開が可能なヘッドレスCMSです。
Astroとの連携
このセクションでは、Contentful SDKを使ってクライアントサイドJavaScriptを使わずにContentfulスペースとAstroを接続する方法を解説します。
必須要件
始めるには以下のものが必要です。
-
Astroプロジェクト - もしAstroプロジェクトをまだ持っていない場合は、自動CLIでAstroをインストールを見ると、すぐに使い始めることができます。
-
ContentfulアカウントとContentfulスペース - もしアカウントを持っていない場合は、フリーアカウントを登録できて、Contentfulスペースを作成できます。既に持っている場合は既存のスペースを利用できます。
-
Contentfulクレデンシャル - ContentfulダッシュボードのSettings > API keysから以下のクレデンシャルを見つけることができます。もしAPIキーが無い場合は、Add API Key を選択して追加してください。
- Contentful space ID - ContentfulスペースのID
- Contentful delivery access token - Contentfulスペースからpublishedコンテンツを利用するためのアクセストークン
- Contentful preview access token - Contentfulスペースからunpublishedコンテンツを利用するためのアクセストークン
クレデンシャルを設定する
ContentfulスペースのクレデンシャルをAstroに追加するために、.env
ファイルをプロジェクトのルートディレクトリに作成して環境変数に追加します。
CONTENTFUL_SPACE_ID=スペースIDCONTENTFUL_DELIVERY_TOKEN=DELIVERY_TOKENCONTENTFUL_PREVIEW_TOKEN=PREVIEW_TOKEN
これで、プロジェクトでこれらの環境変数を利用できます。
もし、Contentfulの環境変数にインテリセンスをつけたい場合は、src
ディレクトリにenv.d.ts
ファイルを作成して、ImportMetaEnv
を以下のように設定します。
interface ImportMetaEnv { readonly CONTENTFUL_SPACE_ID: string; readonly CONTENTFUL_DELIVERY_TOKEN: string; readonly CONTENTFUL_PREVIEW_TOKEN: string;}
環境変数のAstroの.env
ファイルについてをご覧ください。
ルートディレクトリは以下のように作成したファイル含まれているはずです。
ディレクトリsrc/
- env.d.ts
- .env
- astro.config.mjs
- package.json
依存関係をインストールする
Contentfulスペースと接続するために、次の中から好みのパッケージマネージャの1つのコマンドを利用して、以下の両方のパッケージをインストールします。
contentful.js
, Contentful公式のJavaScript用SDKです。rich-text-html-renderer
, ContentfulのリッチテキストフィールドをHTMLに描画するためのパッケージです。
npm install contentful @contentful/rich-text-html-renderer
pnpm add contentful @contentful/rich-text-html-renderer
yarn add contentful @contentful/rich-text-html-renderer
次に、プロジェクトのsrc/lib
にcontentful.ts
というファイルを作成します。
import contentful from "contentful";
export const contentfulClient = contentful.createClient({ space: import.meta.env.CONTENTFUL_SPACE_ID, accessToken: import.meta.env.DEV ? import.meta.env.CONTENTFUL_PREVIEW_TOKEN : import.meta.env.CONTENTFUL_DELIVERY_TOKEN, host: import.meta.env.DEV ? "preview.contentful.com" : "cdn.contentful.com",});
上記のコードスニペットはContentfulクライアントを作成し、.env
ファイルからクレデンシャルを渡しています。
開発中の間は、コンテンツはContentful preview APIから取得します。これはunpublishedのコンテンツをContentfulウェブアプリから閲覧できることを意味します。
ビルド時には、コンテンツはContentful delivery APIから取得します。。これはビルド時にpublishedのコンテンツだけ取得することを意味します。
最終的にルートディレクトリは作成したファイルが以下のように含まれています。
ディレクトリsrc/
- env.d.ts
ディレクトリlib/
- contentful.ts
- .env
- astro.config.mjs
- package.json
取得するデータ
AstroコンポーネントはcontentfulClient
を使ってデータを取得でき、Contentfulアカウントから受信するcontent_type
を指定できます。
例えば、もしタイトルのテキストフィールドとコンテンツのリッチテキストフィールドを持つblogPost
コンテンツタイプがある場合、コンポーネントは以下のようになります。
---import { contentfulClient } from "../lib/contentful";import { documentToHtmlString } from "@contentful/rich-text-html-renderer";import type { EntryFieldTypes } from "contentful";
interface BlogPost { contentTypeId: "blogPost", fields: { title: EntryFieldTypes.Text content: EntryFieldTypes.RichText, }}
const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost",});---<body> {entries.items.map((item) => ( <section> <h2>{item.fields.title}</h2> <article set:html={documentToHtmlString(item.fields.content)}></article> </section> ))}</body>
もしContentfulスペースが空の場合、コンテンツの基本ブログ型の作成方法は Contentfulモデルのセットアップを確認してください。
Contentful documentationからより多くのクエリオプションを見つけることができます。
AstroとContentfulを利用してブログを作成する
上記セットアップで、ContentfulをCMSとして利用するブログが作成できるようになりました。
必須要件
- Contentfulスペース - 本チュートリアルは空のスペースから始めることをお勧めします。既にコンテンツモデルを持っている場合は、そのままコンテンツを使えますが、コードスニペットをコンテンツに合わせた修正が必要になります。
- Contentful SDKと連携されたAstroプロジェクト - より多くのAstroプロジェクトとContentfulのセットアップ方法を知りたい場合は、 Astroとの連携をご覧ください。
Contentfulモデルのセットアップ
Contenfulスペースの内のContent modelセクションで、以下のフィードと値を持つモデルを作成します。
- Name: ブログ記事
- API identifier:
blogPost
- Description: ブログ記事のコンテンツタイプです。
コンテンツタイプを作成したら、Add Fieldボタンを使い以下のパラメータを持つフィールドを5つ追加します。
- テキストフィールド(Text field)
- Name: タイトル
- API identifier:
title
(leave the other parameters as their defaults)
- 日時フィールド(Date and time field)
- Name: 日時
- API identifier:
date
- テキストフィールド(Text field)
- Name: スラッグ
- API identifier:
slug
(他のパラメータはデフォルトのまま残しておきます)
- テキストフィールド(Text field)
- Name: 説明
- API identifier:
description
- リッチテキストフィールド(Rich text field)
- Name: コンテンツ
- API identifier:
content
Saveを押して変更を保存します。
ContentfulスペースのContentセクションで、Add Entryボタンを押して新しいエントリーを作成します。そして、以下のようにフィールドを埋めます。
- Title:
Astro is amazing!
- Slug:
astro-is-amazing
- Description:
Astro is a new static site generator that is blazing fast and easy to use.
- Date:
2022-10-05
- Content:
This is my first blog post!
Publish Publishを押してエントリーを保存します。これではじめてのブログ記事ができました。
より多くのブログ記事を書いてから、お気に入りのエディターに切り替えてAstroを使ってハッキングを始めましょう!
ブログ記事のリストを表示する
BlogPost
というインターフェイスを作成しsrc/lib
にcontentful.ts
というファイルを追加してください。このインターフェイスはContentfulのブログ記事のコンテンツタイプと一致します。ブログエントリーのレスポンスと型を一致させるときに利用します。
import contentful, { EntryFieldTypes } from "contentful";
export interface BlogPost { contentTypeId: "blogPost", fields: { title: EntryFieldTypes.Text content: EntryFieldTypes.RichText, date: EntryFieldTypes.Date, description: EntryFieldTypes.Text, slug: EntryFieldTypes.Text }}
export const contentfulClient = contentful.createClient({ space: import.meta.env.CONTENTFUL_SPACE_ID, accessToken: import.meta.env.DEV ? import.meta.env.CONTENTFUL_PREVIEW_TOKEN : import.meta.env.CONTENTFUL_DELIVERY_TOKEN, host: import.meta.env.DEV ? "preview.contentful.com" : "cdn.contentful.com",});
次に、Contentfulからデータを取得するAstroページを見てみましょう。src/pages/
にあるindex.astro
のサンプルをホームページとして使います。
BlogPost
インターフェイスとsrc/lib/contentful.ts
からcontentfulClient
をインポートします。
ContentfulからコンテンツタイプがblogPost
のすべてのエントリーを取得して、レスポンスを作成するためにBlogPost
インターフェイスを渡します。
---import { contentfulClient } from "../lib/contentful";import type { BlogPost } from "../lib/contentful";
const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost",});---
これでentries.items
にブログ記事が配列として返ってきます。返ってきたデータのフォーマットを変えるためにmap()
を使い新しい配列(posts
)を作成します。
以下の例は、コンテンツモデルからitems.fields
プロパティを返してブログ記事のプレビューを作成して、同時に日付を読みやすいフォーマットに整形しています。
---import { contentfulClient } from "../lib/contentful";import type { BlogPost } from "../lib/contentful";
const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost",});
const posts = entries.items.map((item) => { const { title, date, description, slug } = item.fields; return { title, slug, description, date: new Date(date).toLocaleDateString() };});---
最後に、それぞれのブログ記事のプレビューを表示するためにテンプレートにposts
を利用します。
---import { contentfulClient } from "../lib/contentful";import type { BlogPost } from "../lib/contentful";
const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost",});
const posts = entries.items.map((item) => { const { title, date, description, slug } = item.fields; return { title, slug, description, date: new Date(date).toLocaleDateString() };});---<html lang="en"> <head> <title>My Blog</title> </head> <body> <h1>My Blog</h1> <ul> {posts.map((post) => ( <li> <a href={`/posts/${post.slug}/`}> <h2>{post.title}</h2> </a> <time>{post.date}</time> <p>{post.description}</p> </li> ))} </ul> </body></html>
個別にブログ記事を生成する
上記のようにContentfulからデータを取得しますが、今回は書くブログ記事のためにユニークなページルーティングを作成します。
静的サイトジェネレーター
Astroのデフォルト静的モードを利用している場合、動的ルーティングとgetStaticPaths()
関数を使えます。この関数はビルド時に呼ばれて、ページとなるパスのリストを生成します。
src/pages/posts
に[slug].astro
というファイルを作成します。
index.astro
でやったように、BlogPost
インターフェイスとsrc/lib/contentful.ts
にあるcontentClient
をインポートします。
これで、getStaticPaths()
関数内でデータを取得できます。
---import { contentfulClient } from "../../lib/contentful";import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() { const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", });}---
そして、各アイテムをparams
とprops
プロパティを持つオブジェクトへマッピングさせます。params
プロパティはページURLを生成するために使用され、props
プロパティはページコンポーネントへpropsとして渡されます。
---import { contentfulClient } from "../../lib/contentful";import { documentToHtmlString } from "@contentful/rich-text-html-renderer";import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() { const entries = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", });
const pages = entries.items.map((item) => ({ params: { slug: item.fields.slug }, props: { title: item.fields.title, content: documentToHtmlString(item.fields.content), date: new Date(item.fields.date).toLocaleDateString(), }, })); return pages;}---
params
無いのプロパティは動的ルーティングの名前と一致させる必要があります。ファイル名が[slug].astro
であるため、slug
を利用します。
このサンプルで、props
オブジェクトはページに対して以下の3つのプロパティを渡しています。
- title (文字列)
- content (
documentToHtmlString
を使ってHTMLに変換されたリッチテキストドキュメント) - date (
Date
コンストラクタを使ってフォーマットされた日付)
最後に、ブログ記事を表示するためにページのprops
を利用します。
---import { contentfulClient } from "../../lib/contentful";import { documentToHtmlString } from "@contentful/rich-text-html-renderer";import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() { const { items } = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", }); const pages = items.map((item) => ({ params: { slug: item.fields.slug }, props: { title: item.fields.title, content: documentToHtmlString(item.fields.content), date: new Date(item.fields.date).toLocaleDateString(), }, })); return pages;}
const { content, title, date } = Astro.props;---<html lang="en"> <head> <title>{title}</title> </head> <body> <h1>{title}</h1> <time>{date}</time> <article set:html={content} /> </body></html>
http://localhost:4321/ にアクセスしていずれかの投稿をクリックすると動的ルーティングが動作していることが分かります!
サーバーサイドレンダリング
プロジェクトでSSRを有効にする場合、Contentfulデータを取得するために動的ルーティングで slug
パラメータが利用されます。
src/pages/posts
に[slug].astro
を作成します。URLからスラッグを取得するためにAstro.params
(EN)を使って、以下のようにgetEntries
に渡してあげます。
---import { contentfulClient } from "../../lib/contentful";import type { BlogPost } from "../../lib/contentful";
const { slug } = Astro.params;
const data = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", "fields.slug": slug,});---
もしエントリーが見つからない場合は、 Astro.redirect
を使ってユーザーを404ページにリダイレクトできます。
---import { contentfulClient } from "../../lib/contentful";import type { BlogPost } from "../../lib/contentful";
const { slug } = Astro.params;
try { const data = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", "fields.slug": slug, });} catch (error) { return Astro.redirect("/404");}---
テンプレートセクションで記事データを渡すには、try/catch
ブロックの外にpost
オブジェクトを作成します。
ドキュメントのcontent
をHTMLに変換するためにdocumentToHtmlString()
を使って、日付を成形するためにDateコンストラクターを利用します。title
はそのままで大丈夫です。そしてこれらのプロパティをpost
オブジェクトに追加します。
---import Layout from "../../layouts/Layout.astro";import { contentfulClient } from "../../lib/contentful";import { documentToHtmlString } from "@contentful/rich-text-html-renderer";import type { BlogPost } from "../../lib/contentful";
let post;const { slug } = Astro.params;try { const data = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", "fields.slug": slug, }); const { title, date, content } = data.items[0].fields; post = { title, date: new Date(date).toLocaleDateString(), content: documentToHtmlString(content), };} catch (error) { return Astro.redirect("/404");}---
最後にテンプレートセクションにブログ記事を表示するためにpost
を参照します。
---import Layout from "../../layouts/Layout.astro";import { contentfulClient } from "../../lib/contentful";import { documentToHtmlString } from "@contentful/rich-text-html-renderer";import type { BlogPost } from "../../lib/contentful";
let post;const { slug } = Astro.params;try { const data = await contentfulClient.getEntries<BlogPost>({ content_type: "blogPost", "fields.slug": slug, }); const { title, date, content } = data.items[0].fields; post = { title, date: new Date(date).toLocaleDateString(), content: documentToHtmlString(content), };} catch (error) { return Astro.redirect("/404");}---<html lang="en"> <head> <title>{post?.title}</title> </head> <body> <h1>{post?.title}</h1> <time>{post?.date}</time> <article set:html={post?.content} /> </body></html>
サイトを公開する
ウェブサイトをデプロイするために、デプロイガイドへアクセスして好みのホスティングプロバイダーにあわせた説明に従ってください。
Contentfulの変更時に再ビルドする
もしプロジェクトがAstroのデフォルトである静的モードを使っている場合、コンテンツを変更した時に新しいビルドを行うトリガーをするためのWebhookをセットアップする必要があります。もしNetlifyかVercelをホスティングプロバイダーとして使っている場合、コンテンツイベントから新しいビルドをトリガーするためにWebhook機能を使えます。
Netlify
NetlifyのWebhookをセットアップするためには以下の手順が必要です。
-
ダッシュボードに行き、Build & deployをクリックします。
-
Continuous Deploymentタブから、Build hooks セクションを探しAdd build hookをクリックします。
-
Webhookの名前を指定してビルド時にトリガーされるブランチを選択します。Saveをクリックし生成されたURLをコピーします。
Vercel
VercelのWebhookをセットアップするためには以下の手順が必要です。
-
ダッシュボードへ行き、Settingsをクリックします。
-
Gitタブから、Deploy Hooksセクションを見つけます。
-
Webhookの名前を指定してビルド時にトリガーされるブランチを選択します。Addをクリックして生成されたURLをコピーします。
ContentfulにWebhookを追加する
Contentfulスペースのsettingsで、WebhooksタブをクリックしてAdd WebhookボタンをクリックしてWebhookを作成します。Webhookの名前を指定して、前のセクションでコピーしたWebhook URLをペーストします。最後にSaveをクリックしてWebhookを作成します。
これで、Contentfulでブログ記事を作成しても、新しいビルドがトリガーされブログが更新されます。