ContentfulとAstro
Contentfulはコンテンツの管理と他サービスとの連携、マルチプラットフォームへの公開が可能なヘッドレスCMSです。
Astroとの連携
セクションタイトル: 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-rendererpnpm add contentful @contentful/rich-text-html-rendereryarn 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を利用してブログを作成する
セクションタイトル: AstroとContentfulを利用してブログを作成する上記セットアップで、ContentfulをCMSとして利用するブログが作成できるようになりました。
必須要件
セクションタイトル: 必須要件- Contentfulスペース - 本チュートリアルは空のスペースから始めることをお勧めします。既にコンテンツモデルを持っている場合は、そのままコンテンツを使えますが、コードスニペットをコンテンツに合わせた修正が必要になります。
- Contentful SDKと連携されたAstroプロジェクト - より多くのAstroプロジェクトとContentfulのセットアップ方法を知りたい場合は、 Astroとの連携をご覧ください。
Contentfulモデルのセットアップ
セクションタイトル: 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を使って、以下のように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の変更時に再ビルドする
セクションタイトル: Contentfulの変更時に再ビルドするもしプロジェクトがAstroのデフォルトである静的モードを使っている場合、コンテンツを変更した時に新しいビルドを行うトリガーをするためのWebhookをセットアップする必要があります。もしNetlifyかVercelをホスティングプロバイダーとして使っている場合、コンテンツイベントから新しいビルドをトリガーするためにWebhook機能を使えます。
Netlify
セクションタイトル: NetlifyNetlifyのWebhookをセットアップするためには以下の手順が必要です。
- 
ダッシュボードに行き、Build & deployをクリックします。 
- 
Continuous Deploymentタブから、Build hooks セクションを探しAdd build hookをクリックします。 
- 
Webhookの名前を指定してビルド時にトリガーされるブランチを選択します。Saveをクリックし生成されたURLをコピーします。 
Vercel
セクションタイトル: VercelVercelのWebhookをセットアップするためには以下の手順が必要です。
- 
ダッシュボードへ行き、Settingsをクリックします。 
- 
Gitタブから、Deploy Hooksセクションを見つけます。 
- 
Webhookの名前を指定してビルド時にトリガーされるブランチを選択します。Addをクリックして生成されたURLをコピーします。 
ContentfulにWebhookを追加する
セクションタイトル: ContentfulにWebhookを追加するContentfulスペースのsettingsで、WebhooksタブをクリックしてAdd WebhookボタンをクリックしてWebhookを作成します。Webhookの名前を指定して、前のセクションでコピーしたWebhook URLをペーストします。最後にSaveをクリックしてWebhookを作成します。
これで、Contentfulでブログ記事を作成しても、新しいビルドがトリガーされブログが更新されます。
