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

Posts

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

画面例

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

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

    画面例

  2. [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 を実行する