【Next.js+Vercel+Contentful】Next.jsのPreview Modeを利用してContentfulから記事プレビューを表示する

May 16, 2021

はじめに

このブログはNext.jsで作っており、Vercel上で動いています。
そしてコンテンツの管理はHeadless CMSのContentfulで行っております。
記事自体はNext.jsのStatic Site Generation(SSG)を使って静的ページで表現していますが、それとは別でPreview Modeという機能を使って下書きしている記事をすぐに表示確認できるようにしてみます。

Next.jsでの処理

プレビュー用APIを作成

まずはNext.jsでプレビュー用のAPIを作成します。
これはContentfulからこのAPIを呼び出してプレビューを表示させるためのものです。
具体的にはContentfulのEditorにあるOpen previewボタンを押したときに呼び出されるAPIです。
ここではpages/api/preview.jsを作成します。

// Contentful SDKを利用
const client = require("contentful").createClient({
  space: process.env.NEXT_CONTENTFUL_SPACE_ID,
  accessToken: process.env.NEXT_CONTENTFUL_PREVIEW_ACCESS_TOKEN,
  host: "preview.contentful.com"
});

export default async (req, res) => {

  // シークレットトークンのチェック
  if (req.query.secret !== process.env.NEXT_PREVIEW_SCRET_TOKEN || !req.query.slug) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  // リクエストされたslugの記事があるかチェック
  const entry = await client.getEntries({
    'content_type': 'post',
    'fields.slug': req.query.slug
  });
  const post = entry.items[0];
  if (!post) {
    return res.status(401).json({ message: 'Invalid slug' });
  }

  // Preview Modeの有効化
  res.setPreviewData({});

  // リダイレクト
  res.writeHead(307, { Location: `/post/${post.fields.slug}` });
  res.end();
}

このAPIでは2つのパラメータを受け取っています。

  1. secret: Next.js側とContentful側で共有しているシークレットトークン
  2. slug: プレビューしたい記事のスラグ

するとContentful側から呼び出すAPIは

https://<自分のサイト>/api/preview?secret=<シークレットトークン>&slug=<プレビューしたい記事のスラグ>

といった形になります。

関数の内容としては

  • ちゃんとContentfulからのアクセスか確認するためのシークレットトークンチェック
  • リクエストされたスラグの記事がContentfulに存在するかのチェック
  • Preview Modeの有効化
  • 記事へのリダイレクト

となっています。
私の場合はPostというContent modelにslugというfieldを追加して運用しています。

記事ページをプレビューに対応させる

setPreviewDataで呼び出された記事ページはgetStaticProps内でpreviewフラグを受け取ることができ、そのフラグで判断してContentfulのDRAFT状態を含んだプレビューデータを取得します。
私の場合はpages/post/[slug].jsに記述します。

//...
import { useRouter } from 'next/router';
import ErrorPage from 'next/error';

export default function Post({ post }) {
  const router = useRouter()

  // 記事データが取得できなかったら404を表示
  if (!router.isFallback && !post) {
    return <ErrorPage statusCode={404} />
  }

  // getStaticPropsの実行が終わるまでの表示
  if(router.isFallback) {
    return (
      <div>読み込み中...</div>
    )
  }

  return (
    // コンテンツの表示
  );
}

// PUBLISHEDのみのContent用クライアント
const client = require("contentful").createClient({
  space: process.env.NEXT_CONTENTFUL_SPACE_ID,
  accessToken: process.env.NEXT_CONTENTFUL_ACCESS_TOKEN
});

// DRAFTも含めたContent用クライアント
const previewClient = require("contentful").createClient({
  space: process.env.NEXT_CONTENTFUL_SPACE_ID,
  accessToken: process.env.NEXT_CONTENTFUL_PREVIEW_ACCESS_TOKEN,
  host: "preview.contentful.com"
});  

export async function getStaticProps({ params, preview = false }) {
  const c = preview ? previewClient : client;
  const entry = await c.getEntries({
    'content_type': 'post',
    'fields.slug': params.slug
  });

  return {
    props: {
      preview,
      post: entry.items[0]?.fields ?? null
    },
  };
}

export async function getStaticPaths() {
  const entries = await client.getEntries();
  const posts = entries.items;
  const paths = posts.map((post) => ({ params: { slug: post.fields.slug } }));
  return {
    paths,
    fallback: true, // Preview modeではtrueにする
  };
}

ここで注意するべき点としてgetStaticPathsfallbackをfalseにしてしまうとgetStaticProps が呼ばれないためtrueにします。
そのためコンポーネント側で記事データが取得できなかった場合にエラーページを表示させるなどのハンドリングが必要になってきます。

以上でNext.js側の処理になります。Vercelにデプロイするとプレビュー表示APIが叩けるようになっているはずです。

.env.localの設定値

ローカル環境で試す場合は.env.localにそれぞれ設定値を記述します。
Contentfulの設定値はSettings>API keysから取得できます。
シークレットトークンはツールなどを利用して自分で文字列を生成します。

NEXT_CONTENTFUL_SPACE_ID=<ContentfulのSpace ID>
NEXT_CONTENTFUL_ACCESS_TOKEN=<ContentfulのContent Delivery APIアクセストークン>
NEXT_CONTENTFUL_PREVIEW_ACCESS_TOKEN=<ContentfulのContent Preview APIアクセストークン>
NEXT_PREVIEW_SCRET_TOKEN=<自分で用意したプレビュー用シークレットトークン>

Vercel側の設定

Vercelでは特にやることはなく.env.localで設定した環境変数を登録するだけです。
プロジェクト画面のSettings>Environment Variablesから登録できます。

[Vercel] Settings > Environment Variables

Contentful側の設定

こちらもNext.jsで用意したPreview表示APIを登録するだけです。
Settings>Content previewから登録できます。

[Contentful] Settings > Content preview

設定するURLは

http://<自分のサイト>/api/preview?secret=<生成したシークレットトークン>&slug={entry.fields.slug}

といった形になります。

これでプレビューを表示する準備ができました。
Contentfulで記事を作成するためのEditorを開きOpen previewボタンを押してみるとDRAFT状態の記事プレビューが確認できるはずです。

[Contentful] Open preview

まとめ

Next.jsのPreview modeを使うとわざわざビルドを待たずに下書きの記事を公開前に確認できました。
Contentfulの場合はプレビューデータアクセス用のAPIが用意されてるので便利です。
この機能によってSSGでブログシステムを運用するときの欠点をうまくカバーでき、かつ高パフォーマンスを維持できるのでNext.jsの素晴らしい機能の1つだと思います。