Next.jsとヘッドレスCMSを組み合わせた開発において、「プレビューモード」は必須の機能です。しかし、実装中や運用中にこんなトラブルに遭遇したことはありませんか?
「プレビューを確認した後、普通にサイトを見ようとしたら404エラーになる!」 「記事を公開したはずなのに、なぜか更新が反映されない(ドラフトのまま)」 「毎回ブラウザのクッキーを削除したり、シークレットウィンドウを開くのが面倒くさい...」
これは、Next.jsのプレビュー機能特有の挙動によるものです。今回は、この「プレビューモードから抜け出せない問題」を解決し、開発者にとってもクライアントにとっても親切な「プレビュー解除バー」の実装方法をご紹介します。
なぜNext.jsのプレビュー後に404エラーや不具合が起きるのか
原因は「プレビューモード用クッキー」の残留
Next.jsのプレビューAPI(/api/preview)にアクセスすると、ブラウザには以下の特別なクッキーが保存されます。
__prerender_bypass__next_preview_data
このクッキーが存在する限り、Next.jsはすべてのページリクエストに対して「今はプレビューモードだ」と判断し、context.preview = true として振る舞います。
その結果、通常の本番ページを見に行っても、裏側ではCMSの「下書きデータ(draftKeyあり)」を探しに行ったり、静的キャッシュ(ISR/SSG)を無視して動的取得(SSR相当)を試みたりします。ここでロジックの不整合が起きると、存在しないパスを探して404エラーになったり、表示が崩れたりするのです。
開発者やクライアントを困らせる「解除の手間」
この状態を解消するにはクッキーを削除するしかありません。しかし、非エンジニアであるクライアントに「デベロッパーツールを開いてApplicationタブから...」と説明するのは現実的ではありません。「シークレットウィンドウを使ってください」というのも、UXとしてはスマートではないでしょう。
解決策:ユーザーフレンドリーな「プレビュー解除バー」を作る
最もプロフェッショナルな解決策は、画面上に「現在プレビューモードで表示しています。[終了する]」というバーを表示させることです。
全体の実装フロー
- 解除用API: クッキーを削除するAPIエンドポイントを用意する(Next.js標準)。
- UIコンポーネント: プレビュー中のみ表示されるバーを作成する。
- ページ組込: 各ページでプレビュー状態を判定し、バーを呼び出す。
実装ステップ1:解除用APIエンドポイントの確認
まず、プレビューモードを終了(クッキーを削除)するためのAPIルートが必要です。Next.jsの標準的な構成であれば、pages/api/exit-preview.ts として以下のようなファイルを用意します。
import { NextApiRequest, NextApiResponse } from 'next';
export default async function exit(_: NextApiRequest, res: NextApiResponse) {
// プレビューモードのクッキーを削除
res.clearPreviewData();
// 処理完了後、トップページや元のページへリダイレクト
res.writeHead(307, { Location: '/' });
res.end();
}
このURL /api/exit-preview にアクセスさえすれば、呪縛(クッキー)は解けます。
実装ステップ2:解除バー(UIコンポーネント)の作成
次に、このAPIを叩くためのUIコンポーネント components/ui/PreviewBanner.tsx を作成します。
TypeScript
// components/ui/PreviewBanner.tsx
import Link from 'next/link';
import styles from './PreviewBanner.module.scss';
export const PreviewBanner = () => {
return (
<div className={styles.banner}>
<p>現在プレビューモードで表示しています</p>
<Link href="/api/exit-preview" prefetch={false} className={styles.button}>
プレビューを終了する
</Link>
</div>
);
};
ポイント: prefetch={false} を指定しておきましょう。誤ってホバーしただけで解除APIが叩かれるのを防ぐためです。
スタイリング(SCSS/CSS Modules)
画面最上部に固定し、目立つ色にしておくのがおすすめです。
SCSS
// components/ui/PreviewBanner.module.scss
.banner {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 50px;
background-color: #d63384; // マゼンタなど目立つ色
color: #fff;
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
z-index: 9999; // 最前面に表示
font-weight: bold;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.button {
background: #000;
color: #fff;
padding: 4px 12px;
border-radius: 4px;
font-size: 0.9rem;
text-decoration: none;
transition: opacity 0.2s;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
実装ステップ3:各ページへの組み込み
最後に、詳細ページ(例: pages/insights/[slug].tsx)などでこのコンポーネントを呼び出します。 getStaticProps から渡される preview フラグを利用して、条件付きレンダリングを行います。
TypeScript
// pages/insights/[slug].tsx
import { InferGetStaticPropsType } from 'next';
import { PreviewBanner } from '@/components/ui/PreviewBanner'; // 作成したコンポーネント
// ... getStaticPropsの実装(previewフラグを返す) ...
const InsightDetail = ({ insight, preview }: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<>
{/* ▼ previewがtrueの時だけバーを表示! */}
{preview && <PreviewBanner />}
<main>
{/* 記事コンテンツ */}
<h1>{insight.title}</h1>
{/* ... */}
</main>
</>
);
};
export default InsightDetail;
まとめ:開発体験(DX)と運用体験を向上させよう
プレビュー解除バーを実装することで、以下のメリットが生まれます。
- 404エラーの撲滅: 意図しないプレビュー状態の残留を防げます。
- クライアントへの安心感: 「確認が終わったら黒いボタンを押してください」と伝えるだけで運用が回ります。
- 開発効率アップ: クッキー削除の手間から解放されます。
小さなUIコンポーネントですが、運用のストレスを劇的に減らす「プロのひと手間」です。ぜひ実装してみてください。

