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;
}
};
ポイント
safeParseの活用: バリデーション失敗時に即座にエラーにするのではなく、コンソールに警告を出しつつ、レンダリングを続行させます。- 条件付きロジック:
draftMode().isEnabledが true の場合のみ、この緩和策を適用します。これにより、本番環境の安全性は担保されます。
まとめ
Next.js と microCMS の組み合わせは非常に強力ですが、Zodのようなバリデーションライブラリを挟む場合、「未完成データの扱い」に注意が必要です。
- Route Handlers で正しく Cookie をセットする
- 下書きデータは不完全であることを前提にする
- プレビュー時のみ
safeParseやpartial()を活用してエラーを回避する
これらを意識することで、開発者体験(DX)を損なうことなく、編集者にとっても快適なプレビュー環境を構築できます。

