ドラフトモード

静的レンダリングはヘッドレスCMSからデータを取得するページに便利です。しかし、ヘッドレスCMSでドラフトを執筆中に、すぐにページ上でドラフトを確認したい場合には理想的ではありません。このような場合、Next.jsにビルド時ではなくリクエスト時にページをレンダリングさせ、公開済みコンテンツではなくドラフトコンテンツを取得させたいでしょう。この特定のケースでのみdynamic renderingに切り替えたいはずです。

Next.jsにはこの問題を解決するドラフトモード機能があります。以下はその使用方法です。

ステップ1: ルートハンドラの作成とアクセス

まず、ルートハンドラを作成します。任意の名前(例: app/api/draft/route.ts)で構いません。

次に、next/headersからdraftModeをインポートし、enable()メソッドを呼び出します。

// ドラフトモードを有効化するルートハンドラ
import { draftMode } from 'next/headers'

export async function GET(request: Request) {
  draftMode().enable()
  return new Response('Draft mode is enabled')
}
// ドラフトモードを有効化するルートハンドラ
import { draftMode } from 'next/headers'

export async function GET(request) {
  draftMode().enable()
  return new Response('Draft mode is enabled')
}

これにより、ドラフトモードを有効化するクッキーが設定されます。このクッキーを含む後続のリクエストはドラフトモードをトリガーし、静的生成ページの動作を変更します(詳細は後述)。

/api/draftにアクセスし、ブラウザの開発者ツールで確認することで手動でテストできます。__prerender_bypassという名前のクッキーを持つSet-Cookieレスポンスヘッダーに注目してください。

ヘッドレスCMSから安全にアクセス

実際には、ヘッドレスCMSからこのルートハンドラを安全に呼び出したいでしょう。具体的な手順は使用するヘッドレスCMSによって異なりますが、以下に一般的な手順を示します。

これらの手順は、使用するヘッドレスCMSがカスタムドラフトURLの設定をサポートしていることを前提としています。サポートしていない場合でも、この方法でドラフトURLを保護できますが、ドラフトURLを手動で構築してアクセスする必要があります。

まず、任意のトークン生成器を使用して秘密トークン文字列を作成します。この秘密はNext.jsアプリとヘッドレスCMSのみが知っている状態にします。これにより、CMSにアクセスできない人がドラフトURLにアクセスするのを防ぎます。

次に、ヘッドレスCMSがカスタムドラフトURLの設定をサポートしている場合、以下のようにドラフトURLを指定します。ルートハンドラがapp/api/draft/route.tsにあると仮定します。

Terminal
https://<your-site>/api/draft?secret=<token>&slug=<path>
  • <your-site>はデプロイドメインに置き換えてください。
  • <token>は生成した秘密トークンに置き換えてください。
  • <path>は確認したいページのパスです。/posts/fooを確認したい場合は、&slug=/posts/fooを使用します。

ヘッドレスCMSによっては、ドラフトURLに変数を含めることができ、<path>をCMSのデータに基づいて動的に設定できます(例: &slug=/posts/{entry.fields.slug})。

最後に、ルートハンドラで以下を行います:

  • 秘密トークンが一致し、slugパラメータが存在するか確認します(どちらかが欠けている場合、リクエストは失敗します)。
  • draftMode.enable()を呼び出してクッキーを設定します。
  • その後、ブラウザをslugで指定されたパスにリダイレクトします。
// 秘密トークンとslugを使用するルートハンドラ
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')

  // 秘密トークンとslugパラメータを確認
  // この秘密はこのルートハンドラとCMSのみが知っている必要があります
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }

  // ヘッドレスCMSに問い合わせ、指定された`slug`が存在するか確認
  // getPostBySlugはヘッドレスCMSへの取得ロジックを実装します
  const post = await getPostBySlug(slug)

  // slugが存在しない場合、ドラフトモードの有効化を防ぐ
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }

  // クッキーを設定してドラフトモードを有効化
  draftMode().enable()

  // 取得した投稿のパスにリダイレクト
  // searchParams.slugにはリダイレクトせず、オープンリダイレクト脆弱性を防ぎます
  redirect(post.slug)
}
// 秘密トークンとslugを使用するルートハンドラ
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

export async function GET(request) {
  // クエリ文字列パラメータを解析
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const slug = searchParams.get('slug')

  // 秘密トークンとslugパラメータを確認
  // この秘密はこのルートハンドラとCMSのみが知っている必要があります
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }

  // ヘッドレスCMSに問い合わせ、指定された`slug`が存在するか確認
  // getPostBySlugはヘッドレスCMSへの取得ロジックを実装します
  const post = await getPostBySlug(slug)

  // slugが存在しない場合、ドラフトモードの有効化を防ぐ
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }

  // クッキーを設定してドラフトモードを有効化
  draftMode().enable()

  // 取得した投稿のパスにリダイレクト
  // searchParams.slugにはリダイレクトせず、オープンリダイレクト脆弱性を防ぎます
  redirect(post.slug)
}

成功すると、ブラウザは確認したいパスにドラフトモードクッキー付きでリダイレクトされます。

ステップ2: ページの更新

次のステップは、draftMode().isEnabledの値を確認するようにページを更新することです。

クッキーが設定されたページをリクエストすると、データはビルド時ではなくリクエスト時に取得されます。

さらに、isEnabledの値はtrueになります。

// データを取得するページ
import { draftMode } from 'next/headers'

async function getData() {
  const { isEnabled } = draftMode()

  const url = isEnabled
    ? 'https://draft.example.com'
    : 'https://production.example.com'

  const res = await fetch(url)

  return res.json()
}

export default async function Page() {
  const { title, desc } = await getData()

  return (
    <main>
      <h1>{title}</h1>
      <p>{desc}</p>
    </main>
  )
}
// データを取得するページ
import { draftMode } from 'next/headers'

async function getData() {
  const { isEnabled } = draftMode()

  const url = isEnabled
    ? 'https://draft.example.com'
    : 'https://production.example.com'

  const res = await fetch(url)

  return res.json()
}

export default async function Page() {
  const { title, desc } = await getData()

  return (
    <main>
      <h1>{title}</h1>
      <p>{desc}</p>
    </main>
  )
}

以上です!ヘッドレスCMSから(secretslugを使用して)ドラフトルートハンドラにアクセスするか、手動でアクセスすると、ドラフトコンテンツを確認できるようになります。また、ドラフトを公開せずに更新した場合も、ドラフトを確認できます。

ヘッドレスCMSにこれをドラフトURLとして設定するか、手動でアクセスすると、ドラフトを確認できます。

Terminal
https://<your-site>/api/draft?secret=<token>&slug=<path>

詳細

ドラフトモードクッキーのクリア

デフォルトでは、ドラフトモードセッションはブラウザを閉じると終了します。

ドラフトモードクッキーを手動でクリアするには、draftMode().disable()を呼び出すルートハンドラを作成します:

import { draftMode } from 'next/headers'

export async function GET(request: Request) {
  draftMode().disable()
  return new Response('Draft mode is disabled')
}
import { draftMode } from 'next/headers'

export async function GET(request) {
  draftMode().disable()
  return new Response('Draft mode is disabled')
}

その後、/api/disable-draftにリクエストを送信してルートハンドラを呼び出します。next/linkを使用してこのルートを呼び出す場合、プリフェッチ時に誤ってクッキーを削除しないようにprefetch={false}を渡す必要があります。

next buildごとに一意

next buildを実行するたびに、新しいバイパスクッキー値が生成されます。

これにより、バイパスクッキーが推測されるのを防ぎます。

豆知識: HTTP経由でローカルでドラフトモードをテストするには、ブラウザでサードパーティクッキーとローカルストレージへのアクセスを許可する必要があります。