Next.jsのDraft Modeを利用して、プレビュー環境を作成する
Table of contents
- 記事内で使用している主なソフトウェアのバージョン
- 前提条件
- 概要
- Next.js(App Router)でプレビューをどう実装するか
- パターン1: Draft Modeを利用する場合(SSR環境がある場合)
- パターン2: クライアントコンポーネントを利用する場合(SSR環境がない場合)
- 1. Newt API Tokenを作成する
- 2. プレビューデータの取得メソッドを作成する
- 2-1. 環境変数の設定
- 2-2. プレビューデータの取得メソッドを作成する
- 3. プレビュー用のルートハンドラを作成する
- 4. コンテンツ詳細ページを更新する
- 5. プレビュー設定を行う
- 6. ドラフトモードを無効にする
このチュートリアルでは、Next.jsの Draft Mode と、Newtのプレビュー設定を利用して、プレビュー環境を作成する手順を紹介します。
Next.jsはApp Routerを使用します。
記事内で使用している主なソフトウェアのバージョン
- Next.js(
next
): 14.0.1 - newt-client-js(
newt-client-js
): 3.2.7
前提条件
- Next.jsの App Router を利用していること
- Static Exports を利用していないこと
- 作成したサイトがデプロイ済みであること
- Next.jsの Route Handlers について理解していること
- NewtのJS SDKである newt-client-js の基本的な利用方法について理解していること
Next.jsのセットアップについて知りたい場合は、以下のドキュメントをご確認ください。
- Next.jsの Introduction のドキュメント
- NewtとNext.jsを利用してブログを作成する
概要
Next.jsの Draft Mode を利用して、プレビュー用の Route Handlers を作成し、プレビューデータを取得できるようにします。
また、Newtのコンテンツ編集画面から、作成したプレビュー環境にアクセスできるようにします。
ここでは、以下の流れでプレビュー処理を行うものとして、実装を進めていきます。
- Newtの管理画面から「プレビュー」ボタンをクリックする
- プレビュー用のルートハンドラ
/api/draft
に、secret
とslug
のクエリパラメータをつけてアクセスする secret
とslug
の値を検証し、問題なければコンテンツ詳細ページ/articles/{slug}
にリダイレクトする- コンテンツ詳細ページからプレビューデータ(下書きデータ)を取得して表示する
ここでは NewtとNext.jsを利用してブログを作成する で作成したブログに対して、プレビュー設定を追加する方法を紹介します。
もし設定したいパスが異なる場合は、適宜読み替えながらチュートリアルを進めてください。
Next.js(App Router)でプレビューをどう実装するか
具体的な方法の紹介に入る前に、Next.jsで静的レンダリングされているサイトにプレビューを追加する場合、代表的な2パターンを改めて確認しておきましょう。
パターン1: Draft Modeを利用する場合(SSR環境がある場合)
Vercelにデプロイする場合など、SSR環境がある場合は、こちらのやり方がおすすめです。
Draft Mode を利用することで、プレビューを確認できます。
cookieで状態を制御し、Draft Modeが有効な場合のみ、動的にページを生成できます。
トークンの利用をサーバーサイドのみに限定できるので、コンテンツデータを保護できます。
この記事では、こちらのやり方について解説します。
パターン2: クライアントコンポーネントを利用する場合(SSR環境がない場合)
こちらはS3にデプロイする場合など、Static Exports を利用していて、SSR環境がない場合の方法となります。
プレビュー専用のページを用意し、クライアントコンポーネントを利用することで、動的にページを生成します。
トークンがフロントエンドに露出するため、セキュリティに問題がないか注意が必要です。
この記事では、こちらのやり方について解説はしていません。興味のある方は、Next.jsでクライアントコンポーネントを利用して、プレビュー環境を作成する の記事をご確認ください。
また上記の2つ以外にも、プレビュー用のステージング環境を用意するやり方など、様々な方法があります。サイトの特性や要件に応じて実装方法を選択してください。
1. Newt API Tokenを作成する
はじめに、Newtの管理画面に入り、スペース設定 > APIキー のページからNewt API Tokenを作成します。
※ 下書き中のコンテンツを取得するためには、Newt APIを利用します。
名前と取得対象を決めて「作成」を押します。
2. プレビューデータの取得メソッドを作成する
2-1. 環境変数の設定
Next.jsには環境変数のビルトインサポートがあり、.env.local
を使用して、環境変数をロードできます。Next.jsの環境変数について、詳細は Environment Variables のドキュメントをご確認ください。
.env.local
ファイルを作成しましょう。以下を実際の値で置き換えて定義してください。
NEWT_SPACE_UID
には、プレビューデータの取得対象となるスペースUIDの値を設定します。
NEWT_CDN_API_TOKEN
・NEWT_API_TOKEN
にはNewtの管理画面で作成したTokenの値を設定します。
NEWT_PREVIEW_SECRET
はプレビューリクエストが有効なものであるか検証するために利用します。ご自身で定めたシークレットを入力してください。
1NEWT_SPACE_UID=your-space-uid
2NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx
3NEWT_API_TOKEN=xxxxxxxxxxxxxxx
4NEWT_PREVIEW_SECRET=hogehoge
上記のように定義しておくと、process.env.NEWT_CDN_API_TOKEN
や process.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を設定してください。
1import 'server-only'
2import { createClient } from 'newt-client-js'
3import { cache } from 'react'
4import type { Article } from '@/types/article'
5
6// Newt CDN APIのクライアント(公開コンテンツのみ取得)
7const cdnClient = createClient({
8 spaceUid: process.env.NEWT_SPACE_UID + '',
9 token: process.env.NEWT_CDN_API_TOKEN + '',
10 apiType: 'cdn',
11})
12
13// Newt APIのクライアント(全コンテンツ取得)
14const apiClient = createClient({
15 spaceUid: process.env.NEWT_SPACE_UID + '',
16 token: process.env.NEWT_API_TOKEN + '',
17 apiType: 'api',
18})
19
20export const getArticles = cache(async () => {
21 const { items } = await cdnClient.getContents<Article>({
22 appUid: 'blog',
23 modelUid: 'article',
24 query: {
25 select: ['_id', 'title', 'slug', 'body'],
26 },
27 })
28 return items
29})
30
31export const getArticleBySlug = cache(
32 async (slug: string, isDraft: boolean) => {
33 const client = isDraft ? apiClient : cdnClient
34 const article = await client.getFirstContent<Article>({
35 appUid: 'blog',
36 modelUid: 'article',
37 query: {
38 slug,
39 select: ['_id', 'title', 'slug', 'body'],
40 },
41 })
42 return article
43 }
44)
3. プレビュー用のルートハンドラを作成する
プレビュー用のルートハンドラを作成します。ここではエンドポイントが /api/draft
となるように、app/api/draft/route.ts
というファイルを用意します。この処理では、以下のことを行います。
- リクエストが有効なものか、クエリパラメータのsecretの値で検証する(ここでは2-1で定義した環境変数
NEWT_PREVIEW_SECRET
を利用する) - slugと対応するコンテンツがあるか検証する
- Cookieを設定し、ドラフトモードを有効にする
- 取得した情報からパスを指定してリダイレクトする
1import { draftMode } from 'next/headers'
2import { redirect } from 'next/navigation'
3import { getArticleBySlug } from '@/lib/newt'
4
5export async function GET(request: Request) {
6 const { searchParams } = new URL(request.url)
7 const secret = searchParams.get('secret')
8 const slug = searchParams.get('slug')
9
10 // secretを検証する、slugパラメータの有無を検証する
11 if (secret !== process.env.NEWT_PREVIEW_SECRET || !slug) {
12 return new Response('Invalid token', { status: 401 })
13 }
14
15 // slugと対応するコンテンツがあるか検証する
16 const article = await getArticleBySlug(slug, true)
17 if (!article) {
18 return new Response('Invalid slug', { status: 401 })
19 }
20
21 // Cookieを設定し、ドラフトモードを有効にする
22 draftMode().enable()
23
24 // 取得した情報からパスを指定してリダイレクトする
25 redirect(`/articles/${article.slug}`)
26}
4. コンテンツ詳細ページを更新する
次に、コンテンツ詳細ページの getArticleBySlug
を修正します。
ドラフトモードが有効かどうか isEnabled
を利用して判定し、getArticleBySlug
に引数として渡します。
ここでは、CDN APIで取得できるコンテンツについては generateStaticParams
を利用してビルド時にルートを生成しつつ、その他のパスについては動的にルートを生成するようにします。
※ dynamicParams を false
にすると、動的にルートを生成できなくなり、プレビューの表示時に404エラーとなってしまうので注意してください。
また、ドラフトモードのCookieが設定されている場合、ビルド時ではなくリクエスト時にデータが取得されます。
まとめると、表示される情報は以下のようになります。
コンテンツ詳細ページに直接アクセス | /api/draft 経由でアクセス | |
---|---|---|
公開コンテンツ | 公開時の情報 | 最新の情報 |
下書きコンテンツ | Not Found | 最新の情報 |
以下のように実装します。
1import { draftMode } from 'next/headers'
2import { notFound } from 'next/navigation'
3import { getArticles, getArticleBySlug } from '@/lib/newt'
4import styles from '@/app/page.module.css'
5import type { Metadata } from 'next'
6import type { Article } from '@/types/article'
7
8type Props = {
9 params: {
10 slug: string
11 }
12}
13
14export async function generateStaticParams() {
15 const articles = await getArticles()
16 return articles.map((article) => ({
17 slug: article.slug,
18 }))
19}
20
21export async function generateMetadata({ params }: Props): Promise<Metadata> {
22 const { isEnabled } = draftMode()
23 const { slug } = params
24 const article = await getArticleBySlug(slug, isEnabled)
25
26 return {
27 title: article?.title,
28 description: '投稿詳細ページです',
29 }
30}
31
32export default async function Article({ params }: Props) {
33 const { isEnabled } = draftMode()
34 const { slug } = params
35 const article = await getArticleBySlug(slug, isEnabled)
36 if (!article) {
37 notFound()
38 }
39
40 return (
41 <main className={styles.main}>
42 <h1>{article.title}</h1>
43 <div dangerouslySetInnerHTML={{ __html: article.body }} />
44 </main>
45 )
46}
slugと対応するコンテンツがない場合、notFound を利用して、not-found ファイルの内容を表示します。
以下のように、app/articles/[slug]/not-found.tsx
ファイルを用意しておきます。
1import styles from '@/app/page.module.css'
2
3export default function NotFound() {
4 return (
5 <main className={styles.main}>
6 <h1>Not Found</h1>
7 </main>
8 )
9}
これでNext.jsのプレビュー設定ができました。
変更をコミットして、デプロイしておきましょう。
5. プレビュー設定を行う
続いて、Newtの管理画面に入り、プレビュー設定を行います。
モデル設定の右上から「プレビュー設定」に進みます。
プレビュー用のAPIルート /api/draft
に secret
と slug
のクエリパラメータをつけてアクセスするよう、プレビューURLを指定します。
サイトのドメインが https://nextjs-preview.newt.so
、secretが hogehoge
の場合、プレビューURLは以下のように指定します。
https://nextjs-preview.newt.so/api/draft?secret=hogehoge&slug={slug}
モデルが slug
というフィールドを持つ場合、{slug}
のように記載することで、各コンテンツのslugの値がプレビューURLに展開されます。
これで、Newtのプレビュー設定もできました。
コンテンツ編集画面からプレビューが見れるか確認しましょう。
もし、プレビューが見れない場合は、プレビューURLが正しく指定されているか、tokenやsecretの値が正しいか確認してみてください。
6. ドラフトモードを無効にする
これまでの設定で、プレビューの確認ができるようになりました。
ただ、このままではドラフトモードのCookieが設定されたままとなり、Cookieが有効な間は、常に下書きコンテンツの情報を取得するようになってしまいます。
そこで、最後にドラフトモードを無効にし、Cookieを削除するための設定を紹介します。
まず、プレビューを無効にするためのルートハンドラを以下のように作成します。
1import { draftMode } from 'next/headers'
2
3export async function GET() {
4 draftMode().disable()
5 return new Response('Draft mode is disabled')
6}
これで、/api/disable-draft
にアクセスすると、ドラフトモードが無効化されます。
次に、コンテンツ詳細ページに /api/disable-draft
へのリンクを追加しましょう。
prefetch={false}
を指定するのを忘れないでください。
また、詳細はNext.jsの Clear the Draft Mode cookie のドキュメントをご確認ください。
(省略)
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}>
{isEnabled && (
<Link href="/api/disable-draft" prefetch={false}>
Draft Modeをやめる
</Link>
)}
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</main>
)
}
これで、プレビューを表示した時、以下のように「Draft Modeをやめる」のリンクが表示されるようになります。
プレビューの確認が終わったら、リンクをクリックしてみましょう。
「Draft mode is disabled」と表示されれば、ドラフトモードが無効化し、Cookieが削除されています。
以上ですべての設定ができました。
Next.jsのDraft Modeについて、より詳しく確認したい方は、Next.jsの Draft Mode のドキュメントをご確認ください。