🕶️

Next.js(App Router) + ヘッドレスCMSのプレビュー方法を考える

2023/07/09に公開

はじめに

ヘッドレスCMSを利用する上で、プレビューの仕組みをどのように実現するかは、よく課題としてあげられる問題の一つです。

ヘッドレスCMSと合わせて利用するフレームワークとしてNext.jsが採用される割合は、筆者の主観にはなりますが、近年増加しているように見受けられます。しかしながら、Next.jsの新しい構築方式であるApp Routerと組み合わす際のベストプラクティスについては、まだ確立されていないような印象があります。

今回はApp Routerを利用する場合の実装方式について、要件に応じた最適な方法を考えてみました。

前提条件

今回は例示に利用するヘッドレスCMSとして、自分が一番使い慣れている「microCMS」を選定しています。サービスの概要については、以下のドキュメントサイトをご覧ください。

https://document.microcms.io/

なお今回の記事については、特別microCMSに特化した内容ではなく、他のヘッドレスCMSにおいても応用可能だと考えています。他のヘッドレスCMSを利用している方にとっても、参考となりましたら幸いです。

用語説明

記事内に登場するmicroCMSの用語を説明します。

draftKey

特定のコンテンツの下書き状態を取得するための認証キー。リクエストURLのクエリストリングに含めることで、下書き状態のデータを取得することができます。draftKeyはコンテンツごとに異なり、管理画面から画面プレビューという機能にて、取得することができます。

https://document.microcms.io/content-api/get-content#hab2c474417

下書き全取得のAPIキー

下書き状態のコンテンツも含めてコンテンツを取得する権限が付与されたAPIキー。リクエストヘッダーに含めることで、下書き状態のデータを取得することができます。draftKeyとは異なり、コンテンツ固有に紐づく認証キーではなく、全てのコンテンツに対応する認証キーとなります。

https://document.microcms.io/content-api/x-microcms-api-key

フローチャート

今回は以下のような条件にて整理を行なってみました。

各方式の説明

方式A: クエリストリングを利用したCSR

こちらは完全な静的生成を行い、S3などのストレージにビルドしたファイルを配置する場合の方法です。自分の観測範囲では、Web制作の界隈では、この方式を取られるケースが多いと思います。

クエリストリングからslugdraftKeyを取得し、ブラウザからAPIへのデータフェッチを行い、受け取ったデータをレンダリングします。

ヘッドレスCMSのプレビュー方法としては、一番古くから存在する古典的な方法といえます。

articles/draft/page.tsx
export default function Page() {
  const searchParams = useSearchParams();
  const slug = searchParams.get('slug');
  const draftKey = searchParams.get('draftKey');
  const [article, setArticle] = useState();

  useEffect(() => {
    if (!slug || !draftKey) return;

    const getArticle = async () => {
      const data = await client.getListDetail(
        endpoint: 'blog',
	contentId: slug,
	{
          draftKey: draftKey,
        });
      setArticle(data);
    };
    getArticle();
  }, [slug, draftKey]);

  return article ? <Article data={article} /> : null;
}

メリット

  • サーバーサイドの処理が必要なく、クライアントサイドの処理で完結できる
  • フロント環境が1環境で完結する

デメリット

  • プレビュー表示専用のルーティングを作成する必要がある
  • APIにアクセスするための認証情報(APIキーなど)が露出するため、セキュリティ観点のケアが必要
  • 一覧表示のプレビューに対応できない

方式B: 環境変数を利用したSSR

フロントの環境を2環境用意し、ステージング環境ではサーバーサイドから常にキャッシュを無視してアクセスするようにします。

具体的には環境変数にIS_PREVIEWのような変数を設け、trueの場合は、キャッシュを利用しないように設定します。

fetchlib.js
const cacheMode =
  process.env.IS_PREVIEW === 'true'
    ? {
        cache: 'no-store',
      }
    : {
        next: { revalidate: 60 },
      };
      
// microcms-js-sdkを利用
const article = await client
  .getListDetail({
    endpoint: 'blog',
    contentId: 'hoge',
    customRequestInit: cacheMode,
  })

また、取得データの切り替えには、環境変数に下書き全取得のAPIキーを設定することによって対応します。

// 下書き前取得のAPIキー
MICROCMS_API_KEY=hogehoge

メリット

  • 同じルーティングを使いまわせる
  • 一覧表示のプレビューにも対応できる
  • 他ページへ遷移しても、継続してプレビューを確認できる

デメリット

  • フロント環境を2環境用意する必要がある
  • 第三者のアクセスを防止するため、別途認証やアクセス制御が必要になる

方式C: draftModeを利用したSSR

Next.jsのドキュメントで、標準的な方法として提供されているものです。

https://nextjs.org/docs/app/building-your-application/configuring/draft-mode

任意のURLにアクセスすることで、CookieにdraftModeであることを識別する値を設定します。認証については、ドキュメント内にも記載がある方法ですが、クエリストリング内のsecret値の照合によって、確認を行っています。

app/api/draft/route.ts
// route handler with secret and slug
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  // Parse query string parameters
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const slug = searchParams.get('slug')
 
  // Check the secret and next parameters
  // This secret should only be known to this route handler and the CMS
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }
 
  // Fetch the headless CMS to check if the provided `slug` exists
  // getPostBySlug would implement the required fetching logic to the headless CMS
  const post = await getPostBySlug(slug)
 
  // If the slug doesn't exist prevent draft mode from being enabled
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }
 
  // Enable Draft Mode by setting the cookie
  draftMode().enable()
 
  // Redirect to the path from the fetched post
  // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
  redirect(post.slug)
}

サーバーサイドではCookieの値を元に、本番用のデータを取得するか、下書き用のデータを取得するかを判断して処理します。

draftModeが有効化されている際は、リアルタイムでデータをフェッチしたいので、fetchオプションにキャッシュを利用しないように設定します。

const { isEnabled } = draftMode()

// APIキーの使い分け
const client = isEnabled
  ? draftClient
  : publishClient
 
const cacheMode = isEnabled
  ? {
      cache: 'no-store',
    }
  : {
      next: { revalidate: 60 },
    };
      
// microcms-js-sdkを利用
const article = await client
  .getListDetail({
    endpoint: 'blog',
    contentId: 'hoge',
    customRequestInit: cacheMode,
  })

メリット

  • フロント環境が1環境で完結する
  • 同じルーティングを使いまわせる
  • 一覧表示のプレビューにも対応できる
  • 他ページへ遷移しても、継続してプレビューを確認できる

デメリット

  • やや特殊な実装が必要になる
  • Cookieベースでのコントロールとなるため、挙動が分かりづらい(同じURLでも、下書きの記事が見えたり、見えなかったりする)

方式D: クエリストリングを利用したSSR

こちらはmicroCMSの公式テンプレートでも採用されている方法です。
クエリストリングからdraftKeyを取得し、サーバーサイドからAPIへのデータフェッチを行い、レンダリングを行います。

articles/[slug]/page.tsx
export default async function Page({ params, searchParams }) {
  const cacheMode = searchParams.draftKey
    ? {
        cache: 'no-store',
      }
    : {
        next: { revalidate: 60 },
      };

  // microcms-js-sdkを利用
  const article = await client.getListDetail({
    endpoint: 'blog',
    contentId: params.slug,
    queries: {
      draftKey: searchParams.draftKey,
    },
    customRequestInit: cacheMode,
  });

  return <Article data={data} />;
}

メリット

  • フロント環境が1環境で完結する
  • 同じルーティングを使いまわせる

デメリット

  • 一覧表示のプレビューに対応できない

おわりに

今回は条件別に4種類のプレビューの方法を考えてみました。
もし既にApp Routerを触っている方で、より良いプレビューの方法を知っている方がいたら、ぜひ教えてください!

Discussion