Contentful + Next.js + Vercel でブログを作る

Blog
January 8, 2022

概要

Next.js で Markdown ブログを作るでは、記事を Markdown ファイルで作成したブログを作りました。
この記事では、ヘッドレス CMS である Contentful で記事を管理する Next.js 製のブログを作り、Vercel でホスティングする方法を説明します。

Contentful とは

Contentfulは、ヘッドレス CMS とよばれる API ベースな CMS です。
ヘッドレス CMS なので、Wordpress などの一般的な CMS のようにフロントエンドの部分がありません。
そのため、Next.js や Gatsby のようなクライアント側をレンダリングするフレームワークと組み合わせて使います。

Contentful では、データの置き場(サイト)となる Space の中で、記事データ(Entity)を管理します。
記事データは、Content Model と呼ばれる、タイトル、本文、日付などどんな項目で構成するか?というひな形の定義を元に作られます。

Contentful + Next.js + Vercel なブログを作る流れ

ブログを構築する流れは次のとおりです。

  • Contentful で記事データのひな形を作り、デモデータ用の記事を作る
    • Space を作る
    • API キーを発行する
    • Content Model を定義する
    • 記事を作成する
  • Next.js アプリを作る
    • Next.js のひな形を作る
    • Contentful の API を実行するための準備をする
    • トップページ(記事一覧)を作る
    • 個別ページを作る
  • GitHub に Next.js アプリをコミットし、 Vercel にデプロイする
  • Contentful 側で記事が更新されたときに自動でビルドされるように設定する
    • Vercel で Webhook URL を発行する
    • Contentful に Webhook を追加する
    • 動作を確認する

Contentful で記事データのひな形を作り、デモデータ用の記事を作る

Space を作る

Contentful でアカウントを作ったら、Space を作成します。
アカウントを作成した時点でスペースが作られているので、今回はこれを使用します。
Space は、使用量に応じた「Medium」「Large」の 2 種類のサイズがありますが、無料枠では、「Medium」なスペースをひとつ利用できます。

Space ID は、[Settings] から [General settings]を選択した後の画面で確認できます。

API キーを発行する

Next.js アプリから Contentful で管理している記事データを取得するために、API キーを発行します。

  1. ヘッダーの「Settings」を選択し、「API Keys」を選択します。
  2. [Add API Key]をクリックします。
  3. 「Access API token」画面で、発行する API キーに名前(任意)をつけます。
    画面例
  4. [Save]をクリックします。
  5. 次の値を控えておきます。
    • Space ID
    • Content Delivery API

Content Model を定義する

タイトル、本文、日付などどんな項目で構成するか?というデータ構造を決める、Content Model を作ります。
今回は、タイトル、本文、slug を持つ Content Model を作ります。

  1. ヘッダーの「Content Model」を選択します。

  2. [Add content type]をクリックします。

  3. 「Create new content type」画面で、Content Model の名前を入力します。
    今回は次のように入力しました。「Api Identifier」は、Next.js アプリで指定する「Content Type ID」に相当する値なので、控えておきます。

    • Name: post
    • Api Identifier: post

    画面例

  4. フィールドを追加し、データ構造を定義します。

    NameField IDField説明
    titletitleText(short text)タイトル
    slugslugText(short text)URL パス
    contentcontentText(Rich text)本文

    画面例

  5. [Save]をクリックします。

記事を作成する

Content Model を作成し、記事のひな形ができたので記事を追加します。

  1. ヘッダーの「Content」を選択します。
  2. [Add post] をクリックします。Content Type を選ぶ場合は、「post」を選びます。
  3. デモデータとしての記事の内容を入力します。
  4. [Publish] をクリックし、記事を公開状態にします。

Next.js アプリを作る

Next.js のひな形を作る

Contentful で管理している記事を表示するための、Next.js アプリを作ります。 Next.js アプリを作る流れは、以前書いたNext.js で Markdown ブログを作るをベースにします。

# Next.js アプリのひな形を作成する
$ yarn create next-app practice-nextjs-blog-contentful --typescript
$ cd practice-nextjs-blog-contentful

## ソースコードを src に移動する
$ mkdir src
$ mv pages src/ && mv styles src/

Contentful の API を実行するための準備をする

Contentful とやり取りするための npm パッケージが公開されているので、インストールします。

yarn add contentful

Contentful の API を実行するためのクライアントを生成する関数と、型定義を src/lib/contentful.ts に作ります。
contentType.sys.id には、Content Model を作るときに入力した「Api Identifier」の値(今回は、post)を指定します。

src/lib/contentful.ts
import { createClient } from "contentful";

import { Entry } from "contentful";
import { Document } from "@contentful/rich-text-types";

export interface IPostFields {
  title: string;
  slug: string;
  content: Document;
}

export interface IPost extends Entry<IPostFields> {
  sys: {
    id: string;
    type: string;
    createdAt: string;
    updatedAt: string;
    locale: string;
    contentType: {
      sys: {
        id: "post";
        linkType: "ContentType";
        type: "Link";
      };
    };
  };
}

export const buildClient = () => {
  const client = createClient({
    space: process.env.CF_SPACE_ID || "",
    accessToken: process.env.CF_ACCESS_TOKEN || "",
  });
  return client;
};

環境変数として与える CF_SPACE_IDCF_ACCESS_TOKEN を、.env.local ファイルに定義します。
API キーを発行する時に確認したときの Space ID が「CF_SPACE_ID」、Content Delivery API が、「CF_ACCESS_TOKEN」です。

$ touch .env.local
".env.local"
CF_SPACE_ID=スペースID
CF_ACCESS_TOKEN=APIキー

トップページ(記事一覧)を作る

トップページには、記事の一覧を箇条書きで表示してみましょう。

次の内容で、src/pages/index.tsx を追加します。
12 行目の「content_type」には、「Api Identifier」の値(今回は、post)を指定します。

src/pages/index.tsx
import type { NextPage, GetStaticProps, InferGetStaticPropsType } from "next";
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { buildClient, IPostFields } from "../lib/contentful";
import Link from "next/link";
import { Entry, EntryCollection } from "contentful";

const client = buildClient();

export const getStaticProps: GetStaticProps = async () => {
  const { items }: EntryCollection<IPostFields> = await client.getEntries({
    content_type: "post",
    order: "-sys.createdAt",
  });
  return {
    props: { posts: items },
  };
};

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const Home: NextPage<Props> = ({ posts }) => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <h1 className={styles.title}>Contentful with Next.js</h1>
        <div>
          <ul>
            {posts &&
              posts.map((post: Entry<IPostFields>) => (
                <li key={post.sys.id}>
                  <Link href={post.fields.slug}>
                    <a>
                      <h2>{post.fields.title}</h2>
                    </a>
                  </Link>
                </li>
              ))}
          </ul>
        </div>
      </main>
    </div>
  );
};

export default Home;

getStaticProps() でContenful API を実行して props にその内容を渡します。 このとき、Contentful API で Entry(記事の実体)を取得しています。
「content_type」に「Api Identifier」の値(今回は、post)を指定します。

ローカルでプレビューして、記事の一覧が表示されることを確認します。

$ yarn dev
# http://localhost:3000/ にアクセスする

画面例

個別ページを作る

記事ごとのページを作ります。 今回は本文に Rich text を使うので、装飾がレンダリングされるように「@contentful/rich-text-react-renderer」をインストールします。

yarn add @contentful/rich-text-react-renderer

次の内容で、src/pages/[slug].tsx を追加します。
14 行目の「content_type」には、「Api Identifier」の値(今回は、post)を指定します。

src/pages/[slug].tsx
import type { NextPage, InferGetStaticPropsType, GetStaticPaths } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import ErrorPage from "next/error";
import styles from "../styles/Home.module.css";
import { buildClient, IPostFields } from "../lib/contentful";
import { EntryCollection } from "contentful";
import { documentToReactComponents } from "@contentful/rich-text-react-renderer";

const client = buildClient();

const getPostEntries = async () => {
  const { items }: EntryCollection<IPostFields> = await client.getEntries({
    content_type: "post",
  });
  return items;
};

export const getStaticPaths: GetStaticPaths = async () => {
  const items = await getPostEntries();
  const paths = items.map((item) => {
    return {
      params: { slug: item.fields.slug },
    };
  });
  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps = async () => {
  const items = await getPostEntries();
  return {
    props: {
      posts: items,
    },
  };
};

type Props = InferGetStaticPropsType<typeof getStaticProps>;

const Post: NextPage<Props> = ({ posts }) => {
  const router = useRouter();
  if (!router.isFallback && !posts[0].fields.slug) {
    return <ErrorPage statusCode={404} />;
  }
  const post = posts[0];
  return (
    <div className={styles.container}>
      <Head>
        <title>{post.fields.title}</title>
      </Head>
      <main>
        <h1>{post.fields.title}</h1>
        <div>{documentToReactComponents(post.fields.content)}</div>
      </main>
    </div>
  );
};

export default Post;

getStaticPaths() で、記事のルーティングパスを生成し、getStaticProps() で記事の内容を取得します。 それぞれの関数では、Contenful API で記事を取得している getPostEntries() を呼び出しています。

ローカルでプレビューして、記事の内容が表示されることを確認します。

$ yarn dev
# http://localhost:3000/test/ にアクセスする

画面例

GitHub に Next.js アプリをコミットし、 Vercel にデプロイする

ここまでの内容を GitHub のリポジトリにコミットしておきます。
次に、Vercel と GitHub のリポジトリを連携した Project を作成し、Next.js のアプリをホスティングします。

  1. Vercel にログインします。

  2. 「Import Git Repository」からリポジトリを選択します。
    画面例

  3. 「Configure Project」画面で、プロジェクト名と環境変数(「Enviroment Variables」)を設定します。
    「Enviroment Variables」には、Contentful の Space ID と API キーを設定します。

    • CF_SPACE_ID: Space ID
    • CF_ACCESS_TOKEN: Content Delivery API

    画面例

  4. [Deploy] をクリックします。

ビルドとホスティングが開始され、しばらくすると URL が発行されます。
URL をクリックし、ローカルプレビューしたときと同じようにトップページに記事の一覧が表示されたり、記事の内容ページが表示されたりすることを確認します。

Contentful 側で記事が更新されたときに自動でビルドされるように設定する

このままでは、Vercel を連携した時点の記事しか表示されません。
Contentful で記事を追加、編集、削除したタイミングでビルドして、HTML が生成されるようにします。
Contentful で記事を変更するなどの操作をしたときに、Vercel の Webhook を発火させビルドが実行されるようにします。

Vercel で Webhook URL を発行する

  1. Vercel で、作成した Project を開きます。
  2. 「Settings」タブを選択します。
  3. 左メニューの「Git」を選択し、「Deploy Hooks」で Webhook URL を作成します。
    Webhook の名前(任意、ここでは「post」)とブランチ名(ここでは「main」)を入力します。 画面例
  4. [Create Hook]をクリックします。
  5. 発行された Webhook URL を控えておきます。
    画面例

Contentful に Webhook を追加する

  1. ヘッダーの「Settings」を選択し、「Webhooks」を選択します。
  2. [Add Webhook]をクリックします。
  3. 「Webhook: Unnamed」画面で、Webhook の設定をします。
    • URL: 「POST」を選択し、Vercel で発行した URL を入力します。
      画面例
    • Trigger: 記事に更新があったときにビルドするようにするため、「Entity」のチェックボックスをすべて選択します。 画面例
  4. [Save] をクリックします。

動作を確認する

Contetful でコンテンツを追加し、Vercel でビルドが実行されれば OK です。
Vercel のビルド状態は、「Deployments」で確認できます。
画面例

まとめ

ヘッドレス CMS を使ってブログを作るときの流れを勉強してみたかったので、Contentful + Next.js + Vercel の構成でブログを作ってみました。
どんなヘッドレス CMS を使う場合でも、次のような流れになるかと理解しています。

  • 記事を生成するときに ヘッドレス CMS の API を実行して、記事の情報を取得する
  • 記事を変更したときに、ホスティングしているサービスのビルド用のWebhook を実行する