Next.jsのDraft Modeを利用して、プレビュー環境を作成する

最終更新日:

Table of contents

  1. 記事内で使用している主なソフトウェアのバージョン
  2. 前提条件
  3. 概要
  4. 1. Newt API Tokenを作成する
  5. 2. プレビューデータの取得メソッドを作成する
  6. 2-1. 環境変数の設定
  7. 2-2. プレビューデータの取得メソッドを作成する
  8. 3. プレビュー用のルートハンドラを作成する
  9. 4. コンテンツ詳細ページを更新する
  10. 5. プレビュー設定を行う

このチュートリアルでは、Next.jsの Draft Mode と、Newtのプレビュー設定を利用して、プレビュー環境を作成する手順を紹介します。
Next.jsはApp Routerを使用します。

記事内で使用している主なソフトウェアのバージョン

  • Next.js(next): 13.4.6
  • newt-client-js(newt-client-js): 3.2.4

前提条件

  1. Next.jsの App Router を利用していること
  2. 作成したサイトがデプロイ済みであること
  3. Next.jsの Route Handlers について理解していること
  4. NewtのJS SDKである newt-client-js の基本的な利用方法について理解していること

Next.jsのセットアップについて知りたい場合は、以下のドキュメントをご確認ください。

概要

Next.jsの Draft Mode を利用して、プレビュー用の Route Handlers を作成し、プレビューデータを取得できるようにします。
また、Newtのコンテンツ編集画面から、作成したプレビュー環境にアクセスできるようにします。

nextjs_preview.jpg

ここでは、以下の流れでプレビュー処理を行うものとして、実装を進めていきます。

  • Newtの管理画面から「プレビュー」ボタンをクリックする
  • プレビュー用のルートハンドラ /api/draft に、secretslug のクエリパラメータをつけてアクセスする
  • secretslug の値を検証し、問題なければコンテンツ詳細ページ /articles/{slug} にリダイレクトする
  • コンテンツ詳細ページからプレビューデータ(下書きデータ)を取得して表示する

ここでは NewtとNext.jsを利用してブログを作成する で作成したブログに対して、プレビュー設定を追加する方法を紹介します。
もし設定したいパスが異なる場合は、適宜読み替えながらチュートリアルを進めてください。

1. Newt API Tokenを作成する

はじめに、Newtの管理画面に入り、スペース設定 > APIキー のページからNewt API Tokenを作成します。
※ 下書き中のコンテンツを取得するためには、Newt APIを利用します。

nextjs_preview2.jpg

名前と取得対象を決めて「作成」を押します。

nextjs_preview3.jpg

2. プレビューデータの取得メソッドを作成する

2-1. 環境変数の設定

Next.jsには環境変数のビルトインサポートがあり、.env.local を使用して、環境変数をロードできます。Next.jsの環境変数について、詳細は Environment Variables のドキュメントをご確認ください。

.env.local ファイルを作成しましょう。以下を実際の値で置き換えて定義してください。
NEWT_SPACE_UID には、プレビューデータの取得対象となるスペースUIDの値を設定します。
NEWT_CDN_API_TOKENNEWT_API_TOKEN にはNewtの管理画面で作成したTokenの値を設定します。
NEWT_PREVIEW_SECRET はプレビューリクエストが有効なものであるか検証するために利用します。ご自身で定めたシークレットを入力してください。

NEWT_SPACE_UID=your-space-uid
NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx
NEWT_API_TOKEN=xxxxxxxxxxxxxxx
NEWT_PREVIEW_SECRET=hogehoge

上記のように定義しておくと、process.env.NEWT_CDN_API_TOKENprocess.env.NEWT_API_TOKEN として利用できるようになります。

2-2. プレビューデータの取得メソッドを作成する

Newt CDN API用のクライアントとNewt API用のクライアントをそれぞれ作成します。
token には2-1で設定した環境変数をそれぞれ入力します。

プレビューデータを取得する getArticleBySlug では、引数に isDraft を渡し、true の場合は apiClient を利用して下書きを含む全コンテンツを取得、false の場合は cdnClient を利用して公開コンテンツのみを取得するようにします。
newt-client-js を利用します。

※ appUid・modelUidには取得対象のApp UID・モデルUIDを設定してください。

// lib/newt.ts

import 'server-only'
import { cache } from 'react'
import { createClient } from 'newt-client-js'
import type { Article } from '@/types/article'

// Newt CDN APIのクライアント(公開コンテンツのみ取得)
const cdnClient = createClient({
  spaceUid: process.env.NEWT_SPACE_UID + '',
  token: process.env.NEWT_CDN_API_TOKEN + '',
  apiType: 'cdn',
})

// Newt APIのクライアント(全コンテンツ取得)
const apiClient = createClient({
  spaceUid: process.env.NEWT_SPACE_UID + '',
  token: process.env.NEWT_API_TOKEN + '',
  apiType: 'api',
})

export const getArticles = cache(async () => {
  const { items } = await cdnClient.getContents<Article>({
    appUid: 'blog',
    modelUid: 'article',
    query: {
      select: ['_id', 'title', 'slug', 'body'],
    },
  })
  return items
})

export const getArticleBySlug = cache(
  async (slug: string, isDraft: boolean) => {
    const client = isDraft ? apiClient : cdnClient
    const article = await client.getFirstContent<Article>({
      appUid: 'blog',
      modelUid: 'article',
      query: {
        slug,
        select: ['_id', 'title', 'slug', 'body'],
      },
    })
    return article
  }
)

3. プレビュー用のルートハンドラを作成する

プレビュー用のルートハンドラを作成します。ここではエンドポイントが /api/draft となるように、app/api/draft/route.ts というファイルを用意します。この処理では、以下のことを行います。

  • リクエストが有効なものか、クエリパラメータのsecretの値で検証する(ここでは2-1で定義した環境変数 NEWT_PREVIEW_SECRET を利用する)
  • slugと対応するコンテンツがあるか検証する
  • Cookieを設定し、ドラフトモードを有効にする
  • 取得した情報からパスを指定してリダイレクトする
// app/api/draft/route.ts

import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
import { getArticleBySlug } from '@/lib/newt'

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

  // secretを検証する、slugパラメータの有無を検証する
  if (secret !== process.env.NEWT_PREVIEW_SECRET || !slug) {
    return new Response('Invalid token', { status: 401 })
  }

  // slugと対応するコンテンツがあるか検証する
  const article = await getArticleBySlug(slug, true)
  if (!article) {
    return new Response('Invalid slug', { status: 401 })
  }

  // Cookieを設定し、ドラフトモードを有効にする
  draftMode().enable()

  // 取得した情報からパスを指定してリダイレクトする
  redirect(`/articles/${article.slug}`)
}

4. コンテンツ詳細ページを更新する

次に、コンテンツ詳細ページの getArticleBySlug を修正します。
ドラフトモードが有効かどうか isEnabled を利用して判定し、getArticleBySlug に引数として渡します。

ここでは、CDN APIで取得できるコンテンツについては generateStaticParams を利用してビルド時にルートを生成しつつ、その他のパスについては動的にルートを生成するようにします。
dynamicParamsfalse にすると、動的にルートを生成できなくなり、プレビューの表示時に404エラーとなってしまうので注意してください。

また、ドラフトモードのCookieが設定されている場合、ビルド時ではなくリクエスト時にデータが取得されます。

まとめると、表示される情報は以下のようになります。

コンテンツ詳細ページに直接アクセス /api/draft 経由でアクセス
公開コンテンツ 公開時の情報 最新の情報
下書きコンテンツ Not Found 最新の情報

以下のように実装します。

// app/articles/[slug]/page.tsx

import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
import { getArticles, getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'
import type { Metadata } from 'next'
import type { Article } from '@/types/article'

type Props = {
  params: {
    slug: string
  }
}

export async function generateStaticParams() {
  const articles = await getArticles()
  return articles.map((article) => ({
    slug: article.slug,
  }))
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { isEnabled } = draftMode()
  const { slug } = params
  const article = await getArticleBySlug(slug, isEnabled)

  return {
    title: article?.title,
    description: '投稿詳細ページです',
  }
}

export default async function Article({ params }: Props) {
  const { isEnabled } = draftMode()
  const { slug } = params
  const article = await getArticleBySlug(slug, isEnabled)
  if (!article) {
    notFound()
  }

  return (
    <main className={styles.main}>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.body }} />
    </main>
  )
}

slugと対応するコンテンツがない場合、notFound を利用して、not-found ファイルの内容を表示します。
以下のように、app/articles/[slug]/not-found.tsx ファイルを用意しておきます。

// app/articles/[slug]/not-found.tsx

import styles from '@/app/page.module.css'

export default function NotFound() {
  return (
    <main className={styles.main}>
      <h1>Not Found</h1>
    </main>
  )
}

これでNext.jsの設定は終了です。変更をコミットして、デプロイしておきましょう。

5. プレビュー設定を行う

最後に、Newtの管理画面に入り、プレビュー設定を行います。
モデル設定の右上から「プレビュー設定」に進みます。

プレビュー用のAPIルート /api/draftsecretslug のクエリパラメータをつけてアクセスするよう、プレビューURLを指定します。
サイトのドメインが https://nextjs-preview.newt.so、secretが hogehoge の場合、プレビューURLは以下のように指定します。

https://nextjs-preview.newt.so/api/draft?secret=hogehoge&slug={slug}

モデルが slug というフィールドを持つ場合、{slug} のように記載することで、各コンテンツのslugの値がプレビューURLに展開されます。

nextjs_preview4.jpg

以上ですべての設定ができました。
コンテンツ編集画面からプレビューが見れるか確認しましょう。

もし、プレビューが見れない場合は、プレビューURLが正しく指定されているか、tokenやsecretの値が正しいか確認してみてください。

また、Next.jsのDraft Modeについて、より詳しく確認したい方は、Next.jsの Draft Mode のドキュメントをご確認ください。

NewtMade in Newt