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