Next.js + microCMSでプレビュー機能を実装する方法【Zodバリデーションの罠を回避】

Next.js + microCMSでプレビュー機能を実装する方法【Zodバリデーションの罠を回避】 のビジュアル

Next.js App RouterとmicroCMSを組み合わせたプレビュー機能(Draft Mode)の実装手順を解説します。
特に、TypeScriptとZodを使用して厳格なスキーマ定義を行っている場合に発生しがちな「下書きデータのバリデーションエラー」という落とし穴に焦点を当て、型安全性を維持したまま解決する具体的なコード例を紹介します。

  • Next.js Draft ModeApp Routerの機能を使用し、Cookieベースでプレビュー状態を管理する手法。
  • Zodの落とし穴必須項目の未入力など、下書きデータの不完全さが原因でビルドやレンダリングが落ちる問題。
  • 解決策プレビュー判定を行い、safeParse または partial スキーマを活用してエラーを回避する。

Next.js App Routerでのプレビュー機能の基本

Next.js (App Router) では、Draft Mode という機能を使って、静的生成(SSG/ISR)を行っているページでも、特定のCookieを持っている場合のみ動的に最新の下書きデータを取得して表示することが可能です。

まずは基本となる Route Handler の設定を見ていきましょう。

Route Handlersの設定 (api/draft/route.ts)

microCMSの管理画面から「プレビュー」ボタンを押した際にアクセスされるエンドポイントを作成します。

// app/api/draft/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get('secret');
  const slug = searchParams.get('slug');
  const contentId = searchParams.get('contentId');

  // シークレットの検証
  if (secret !== process.env.MICROCMS_SERVICE_DOMAIN || !slug || !contentId) {
    return new Response('Invalid token', { status: 401 });
  }

  // Draft Modeを有効化(Cookieがセットされます)
  draftMode().enable();

  // 実際の記事ページへリダイレクト
  redirect(`/blog/${contentId}?${searchParams.toString()}`);
}

この設定により、microCMS側で設定したプレビューURLにアクセスすると、Next.js側でCookieがセットされ、対象のページへリダイレクトされます。

陥りやすい「Zodバリデーションの罠」とは?

ここで多くの開発者が直面するのが、「Zodのバリデーションエラー」です。

通常、APIレスポンスの型定義には z.object({...}) などを使って厳格なスキーマを定義しているはずです。しかし、microCMSの「下書き」状態の記事は、必須項目が空であったり、参照フィールドが解決されていなかったりすることがあります。

下書きデータと公開データの違い

例えば、以下のようなスキーマを定義していたとします。

TypeScript

const ArticleSchema = z.object({
  id: z.string(),
  title: z.string(),
  thumbnail: z.object({
    url: z.string().url(),
    height: z.number(),
    width: z.number(),
  }),
  // 公開時は必須だが、執筆中は未設定かもしれない
  category: z.object({
    id: z.string(),
    name: z.string(),
  }),
});

記事を執筆中、まだ「サムネイル」や「カテゴリ」を設定せずに「下書き保存」をしてプレビューを見ようとすると、microCMSからは該当フィールドが null または欠落した状態で返ってきます。

その結果、Zodが ZodError を吐き、プレビュー画面が 500エラー でクラッシュしてしまうのです。

【解決策】プレビュー時のみバリデーションを緩和する

この問題を解決するために、「型定義をすべて optional にする(緩くする)」のは得策ではありません。本番環境では堅牢性を保ちたいからです。

推奨されるのは、「Draft Modeが有効な時だけ、バリデーションを緩和する(あるいは safeParse で逃げる)」というアプローチです。

safeParseを使ったフォールバック実装

データ取得関数の中で draftMode().isEnabled を判定し、処理を分岐させます。

TypeScript

import { draftMode } from 'next/headers';
import { createClient } from 'microcms-js-sdk';
import { z } from 'zod';

// 通常の厳格なスキーマ
const ArticleSchema = z.object({
  id: z.string(),
  title: z.string(),
  content: z.string(),
  thumbnail: z.object({ url: z.string() }),
});

// バリデーション失敗時のダミーデータ(プレビュー用)
const fallbackData = {
  id: 'preview',
  title: '(No Title)',
  content: '<p>Content is missing</p>',
  thumbnail: { url: '/placeholder.png' },
};

export const getDetail = async (contentId: string, queries?: any) => {
  const { isEnabled } = draftMode();
  
  const client = createClient({
    serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN || '',
    apiKey: process.env.MICROCMS_API_KEY || '',
  });

  try {
    const data = await client.get({
      endpoint: 'blogs',
      contentId,
      queries,
    });

    if (isEnabled) {
      // プレビュー時は safeParse で検証し、失敗しても落ちないようにする
      const result = ArticleSchema.safeParse(data);
      if (!result.success) {
        console.warn('Preview validation failed:', result.error);
        // 部分的にデータが欠損していても、表示できる部分だけ表示するか
        // 生のデータを無理やり渡す(型アサーションが必要な場合あり)
        return data as z.infer<typeof ArticleSchema>;
      }
      return result.data;
    }

    // 本番時は厳格にチェック(失敗=例外スローでOK)
    return ArticleSchema.parse(data);

  } catch (error) {
    if (isEnabled) {
      // プレビュー時はエラー画面を見せないためのフォールバック
      return fallbackData;
    }
    throw error;
  }
};

ポイント

  1. safeParse の活用: バリデーション失敗時に即座にエラーにするのではなく、コンソールに警告を出しつつ、レンダリングを続行させます。
  2. 条件付きロジック: draftMode().isEnabled が true の場合のみ、この緩和策を適用します。これにより、本番環境の安全性は担保されます。

まとめ

Next.js と microCMS の組み合わせは非常に強力ですが、Zodのようなバリデーションライブラリを挟む場合、「未完成データの扱い」に注意が必要です。

  • Route Handlers で正しく Cookie をセットする
  • 下書きデータは不完全であることを前提にする
  • プレビュー時のみ safeParsepartial() を活用してエラーを回避する

これらを意識することで、開発者体験(DX)を損なうことなく、編集者にとっても快適なプレビュー環境を構築できます。

参考リンク

この記事は役に立ちましたか?