How to implement preview functionality with Next.js + microCMS [Avoiding Zod validation pitfalls]

How to implement preview functionality with Next.js + microCMS [Avoiding Zod validation pitfalls] のビジュアル

This article explains how to implement the preview functionality (Draft Mode) using a combination of Next.js App Router and microCMS.
In particular, it focuses on the common pitfall of 'draft data validation errors' that often occur when strict schema definitions are used with TypeScript and Zod, and introduces concrete code examples to resolve these errors while maintaining type safety.

  • 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)を損なうことなく、編集者にとっても快適なプレビュー環境を構築できます。

参考リンク

Was this article helpful?