コンテンツにスキップ

ContentfulとAstro

Contentfulはコンテンツの管理と他サービスとの連携、マルチプラットフォームへの公開が可能なヘッドレスCMSです。

Astroとの連携

このセクションでは、Contentful SDKを使ってクライアントサイドJavaScriptを使わずにContentfulスペースとAstroを接続する方法を解説します。

必須要件

始めるには以下のものが必要です。

  1. Astroプロジェクト - もしAstroプロジェクトをまだ持っていない場合は、自動CLIでAstroをインストールを見ると、すぐに使い始めることができます。

  2. ContentfulアカウントとContentfulスペース - もしアカウントを持っていない場合は、フリーアカウントを登録できて、Contentfulスペースを作成できます。既に持っている場合は既存のスペースを利用できます。

  3. 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ファイルをプロジェクトのルートディレクトリに作成して環境変数に追加します。

.env
CONTENTFUL_SPACE_ID=スペースID
CONTENTFUL_DELIVERY_TOKEN=DELIVERY_TOKEN
CONTENTFUL_PREVIEW_TOKEN=PREVIEW_TOKEN

これで、プロジェクトでこれらの環境変数を利用できます。

もし、Contentfulの環境変数にインテリセンスをつけたい場合は、srcディレクトリにenv.d.tsファイルを作成して、ImportMetaEnvを以下のように設定します。

src/env.d.ts
interface ImportMetaEnv {
readonly CONTENTFUL_SPACE_ID: string;
readonly CONTENTFUL_DELIVERY_TOKEN: string;
readonly CONTENTFUL_PREVIEW_TOKEN: string;
}

ルートディレクトリは以下のように作成したファイル含まれているはずです。

  • ディレクトリsrc/
    • env.d.ts
  • .env
  • astro.config.mjs
  • package.json

依存関係をインストールする

Contentfulスペースと接続するために、次の中から好みのパッケージマネージャの1つのコマンドを利用して、以下の両方のパッケージをインストールします。

  • contentful.js, Contentful公式のJavaScript用SDKです。
  • rich-text-html-renderer, ContentfulのリッチテキストフィールドをHTMLに描画するためのパッケージです。
Terminal window
npm install contentful @contentful/rich-text-html-renderer

次に、プロジェクトのsrc/libcontentful.tsというファイルを作成します。

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ファイルからクレデンシャルを渡しています。

最終的にルートディレクトリは作成したファイルが以下のように含まれています。

  • ディレクトリ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 documentationからより多くのクエリオプションを見つけることができます。

AstroとContentfulを利用してブログを作成する

上記セットアップで、ContentfulをCMSとして利用するブログが作成できるようになりました。

必須要件

  1. Contentfulスペース - 本チュートリアルは空のスペースから始めることをお勧めします。既にコンテンツモデルを持っている場合は、そのままコンテンツを使えますが、コードスニペットをコンテンツに合わせた修正が必要になります。
  2. Contentful SDKと連携されたAstroプロジェクト - より多くのAstroプロジェクトとContentfulのセットアップ方法を知りたい場合は、 Astroとの連携をご覧ください。

Contentfulモデルのセットアップ

Contenfulスペースの内のContent modelセクションで、以下のフィードと値を持つモデルを作成します。

  • Name: ブログ記事
  • API identifier: blogPost
  • Description: ブログ記事のコンテンツタイプです。

コンテンツタイプを作成したら、Add Fieldボタンを使い以下のパラメータを持つフィールドを5つ追加します。

  1. テキストフィールド(Text field)
    • Name: タイトル
    • API identifier: title (leave the other parameters as their defaults)
  2. 日時フィールド(Date and time field)
    • Name: 日時
    • API identifier: date
  3. テキストフィールド(Text field)
    • Name: スラッグ
    • API identifier: slug (他のパラメータはデフォルトのまま残しておきます)
  4. テキストフィールド(Text field)
    • Name: 説明
    • API identifier: description
  5. リッチテキストフィールド(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/libcontentful.tsというファイルを追加してください。このインターフェイスはContentfulのブログ記事のコンテンツタイプと一致します。ブログエントリーのレスポンスと型を一致させるときに利用します。

src/lib/contentful.ts
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インターフェイスを渡します。

src/pages/index.astro
---
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プロパティを返してブログ記事のプレビューを作成して、同時に日付を読みやすいフォーマットに整形しています。

src/pages/index.astro
---
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を利用します。

src/pages/index.astro
---
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()関数内でデータを取得できます。

src/pages/posts/[slug].astro
---
import { contentfulClient } from "../../lib/contentful";
import type { BlogPost } from "../../lib/contentful";
export async function getStaticPaths() {
const entries = await contentfulClient.getEntries<BlogPost>({
content_type: "blogPost",
});
}
---

そして、各アイテムをparamspropsプロパティを持つオブジェクトへマッピングさせます。paramsプロパティはページURLを生成するために使用され、propsプロパティはページコンポーネントへpropsとして渡されます。

src/pages/posts/[slug].astro
---
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を利用します。

src/pages/posts/[slug].astro
---
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に渡してあげます。

src/pages/posts/[slug].astro
---
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ページにリダイレクトできます。

src/pages/posts/[slug].astro
---
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オブジェクトに追加します。

src/pages/posts/[slug].astro
---
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を参照します。

src/pages/posts/[slug].astro
---
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をセットアップするためには以下の手順が必要です。

  1. ダッシュボードに行き、Build & deployをクリックします。

  2. Continuous Deploymentタブから、Build hooks セクションを探しAdd build hookをクリックします。

  3. Webhookの名前を指定してビルド時にトリガーされるブランチを選択します。Saveをクリックし生成されたURLをコピーします。

Vercel

VercelのWebhookをセットアップするためには以下の手順が必要です。

  1. ダッシュボードへ行き、Settingsをクリックします。

  2. Gitタブから、Deploy Hooksセクションを見つけます。

  3. Webhookの名前を指定してビルド時にトリガーされるブランチを選択します。Addをクリックして生成されたURLをコピーします。

ContentfulにWebhookを追加する

Contentfulスペースのsettingsで、WebhooksタブをクリックしてAdd WebhookボタンをクリックしてWebhookを作成します。Webhookの名前を指定して、前のセクションでコピーしたWebhook URLをペーストします。最後にSaveをクリックしてWebhookを作成します。

これで、Contentfulでブログ記事を作成しても、新しいビルドがトリガーされブログが更新されます。

その他のCMSガイド