Next.jsのPreview Modeを使ってmicroCMSのプレビュー機能を実装してみた
技術構成
- API: microCMS(ヘッドレスCMS)
- Next.js
- Vercel
- TypeScript
はじめに
プレビュー機能は、Next.jsのPreview Modeのおかげで、実装自体は簡単になりました。ですが、「現在どのプロセスを実行中で、どのシステムやサービスに対してどんな操作を行っているのか?」 の把握に少し時間がかかりました。
なのでこの記事では、プレビュー機能を実装する過程を分けて解説し、図解した上で今はどの過程かを示して書くようにしました。特に初めての方が迷わずに進められるように心がけました。
プレビュー機能とは?
プレビュー機能は、コンテンツの公開前に、そのコンテンツがウェブサイト上でどのように見えるかを確認できる機能です。この機能により、下書き状態や編集中の記事を、実際の公開環境と同じ条件下で閲覧することが可能となります。
プレビュー機能をどう実現しているか?
結論: サーバーサイドレンダリング(SSR)で実現しました(コンテンツを即座に確認できるように)。
少し話はそれますが、Next.jsのStatic Generationは、microCMSなどのヘッドレスCMSからデータを取得する場合、ビルド時にAPI経由でコンテンツを取得してページを生成し、ユーザーに対して速くコンテンツを提供できます。これは、リクエストごとにサーバーがページをリアルタイムで生成するサーバーサイドレンダリング(SSR)というページ生成手法でサーバー負荷を減らすことも期待できます。
ですが、下書きをすぐにページ上で確認(プレビュー)したい場合には、Static Generationだと多少時間がかかるかもしれません。
下書きなど特定のケースに対してのみ、Static Generationではなく、リクエスト時にページをレンダリングしたいときに役立つのがNext.jsのPreview Modeです。この機能を利用すると、静的生成のロジックを利用しつつサーバーサイドレンダリングを行い、プレビュー機能を実現しています。
Next.jsのPreview Modeとは
Next.jsのPreview Modeは、Next.js 9.3からサポートされました。公式ドキュメントによると、静的生成(Static Generation)を使用している場合でも、ヘッドレスCMSからのドラフトコンテンツをリアルタイムでプレビューできるようにする機能らしいです。
この機能がサポートされていなければ、別途、プレビュー記事を確認するための環境を構築する必要があるだろうし、そのための実装やセキュリティの考慮をしないといけません。同じ本番環境で記事プレビューできる点が素晴らしいですね。
仕組み
Next.jsのPreview Modeを利用すると、静的に生成されたサイトでもリアルタイムに最新コンテンツを表示できます。開発者がプレビューAPIルートを設定し、CMSからのリクエストでプレビューモードを有効にすることで、サーバーはリクエスト毎に指定ページをレンダリングします。
その際に、setPreviewDataというメソッドを使うことで、プレビューモード時のみ特定のデータの取得を可能にし、ブラウザへのクッキー設定により、プレビューモードの状態管理を行います。これにより、getStaticPropsの挙動を制御し、下書き機能を提供しています。
処理の流れ(シーケンス図)
下書きを反映させるプロセスは、複数のステップを経て実現しています。
- ユーザー(コンテンツ管理者)がプレビューリンクをクリック
- プレビューのリクエストを送信(クエリパラメータには記事のslug、そしてdraftKeyを含む)
- プレビューモード有効化 & リダイレクト
- Next.jsでgetStaticPropsが実行
- Next.jsのgetStaticProps内でプレビューデータを使用して下書きコンテンツをフェッチ
- 下書きコンテンツをmicroCMSから取得
- フェッチされた下書きコンテンツがブラウザに表示される
順番に説明していきます。
microCMSからNext.jsのPreviewエンドポイントのリクエスト
処理の流れでいうと1と2
microCMSからNext.jsのAPI Routesを通じてPreview機能を実行するエンドポイントへリクエストするには、まずmicroCMSの「画面プレビュー設定」機能を利用します。「画面プレビュー設定」にてリクエストするエンドポイントを設定します。
設定をしたら、記事詳細ページの管理画面の「画面プレビュー」をクリック。するとプレビューが実行されます。
この設定により、コンテンツ編集者は管理画面から直接プレビューページにアクセスし、リアルタイムでの変更を確認することができます。
ここでの重要なのは、microCMSの管理画面でプレビュー用のURLを設定し、そのURLがNext.jsのAPI RoutesのPreview機能を実行するエンドポイントを指していることを確認することです。
次は、Next.jsでのプレビュー機能の実装です。
実装手順
実装手順は以下の通りです
- Next.jsのAPI Routesでプレビュー用の関数の作成
- Next.jsのPage側のgetStaticProps内の処理
- プレビューモードの終了とCookieの削除
- プレビューモードの表示管理
順番に説明していきます
Next.jsのAPI Routesでプレビュー用の関数の作成
処理の流れでいうと2と3
/pages/api/
以下に配置されたファイルはサーバレス関数として動作します。プレビューモードを実現するために、この機能を利用してプレビュー用のAPIを作成します。
以下のコードは、microCMSからのプレビューリクエストを処理し、プレビューモードを有効にするAPIの実装を示しています。
プレビューモードを有効にするAPIの実装例
pages/api/preview.ts
import type { NextApiHandler } from 'next';
import fetch from 'node-fetch';
const handlePreviewRequest: NextApiHandler = async (req, res) => {
const slug = typeof req.query.slug === 'string' ? req.query.slug : '';
const draftKey = typeof req.query.draftKey === 'string' ? req.query.draftKey : '';
const type = typeof req.query.type === 'string' ? req.query.type : 'posts';
if (!slug) {
return res.status(404).end();
}
const apiUrl = `https://${process.env.MICROCMS_SERVICE_DOMAIN || ''}.microcms.io/api/v1/${type}/${slug}?fields=id&draftKey=${draftKey}`;
const content = await fetch(apiUrl, {
headers: { 'X-MICROCMS-API-KEY': process.env.MICROCMS_API_KEY || '' },
}).then((res) => res.json()).catch((error) => {
console.log(error);
return null;
});
if (!content) {
return res.status(404).json({ message: 'Invalid slug' });
}
res.setPreviewData({ slug: content.id, draftKey });
res.writeHead(307, { Location: `/${type}/${slug}` });
res.end('Preview mode enabled');
};
export default handlePreviewRequest;
今回は、変数typeを使用して、microCMSの異なる記事のエンドポイントごとtypeを分けていますが、1つのエンドポイントを使用している場合は、この処理や変数は不要です。
クエリとしてslug(記事のID)、draftKey(下書き用のキー)、およびtype(コンテンツの種類やカテゴリー)が渡され、これらを用いてmicroCMSのAPIを呼び出し、該当する下書き記事のデータを取得します。
記事データが正しく取得できた場合、res.setPreviewData()を用いてプレビューデータをセットし、プレビューモードを有効化します。その後、リダイレクトさせます。
このプロセスを通じて、プレビューモードが有効化された状態で記事のパスにリダイレクトされ、__prerender_bypass
と__next_preview_data
というcookieがブラウザに付与されます。これにより、Next.jsのフロントエンド側でgetStaticPropsが呼び出された際に、プレビューモードが有効であることを検知し、下書き状態のコンテンツを取得して表示することが可能になります。
安全なリダイレクト処理の実装
処理の流れでいうと3
オープンリダイレクトの脆弱性回避のために、req.query.slug
ではなく content.id
を使いましょう。オープンリダイレクトは、攻撃者がリダイレクト先のURLを操作できる脆弱性で、ユーザーを悪意あるページに誘導することが可能になります。
具体的には、ユーザーからのリクエスト(req.query.slug)を直接リダイレクト先のURLとして使用するのではなく、サーバー側でAPIなどを通じて取得したデータ(content.id)を用いてリダイレクト先を指定します。
Next.jsのPage側のgetStaticProps内の処理
処理の流れでいうと4,5,6
リダイレクトして記事ページに辿り着いた後、getStaticPropsがビルド時、またはリクエスト時にサーバーサイドで実行され、必要なデータを取得します。
先ほどのcookie付きでこのページに辿り着いた場合、getStaticProps
の引数(例 context)には以下の情報が含まれます
- context.previewはtrueに設定されます
- これは、現在プレビューモードが有効であることを示します
- context.previewDataには、setPreviewData関数によって設定されたプレビューデータが含まれます
- このデータには、slugやdraftKey、typeなどの情報が含まれます
これらの情報を利用して、今度はmicroCMSから下書き状態のコンテンツを取得することができます。
以下は実装です。
Page側のgetStaticProps内の実装例
getStaticPropsとgetStaticPathsの処理に焦点を当て、プレビューモードが有効な場合に下書き状態のコンテンツを取得する方法を示します。
src/pages/posts/[contentId]/index.tsx
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
export const getStaticPaths: GetStaticPaths = () => {
return {
paths: [],
fallback: 'blocking',
};
};
export const getStaticProps: GetStaticProps = async ({ params, previewData }) => {
const contentId = params?.contentId;
if (!contentId) {
return { notFound: true };
}
const draftKey = previewData?.draftKey;
const post = await fetch(
`https://YOUR_MICROCMS_DOMAIN.microcms.io/api/v1/posts/${contentId}${
draftKey ? `?draftKey=${draftKey}` : ''
}`,
{
headers: { 'X-MICROCMS-API-KEY': process.env.MICROCMS_API_KEY || '' },
}
).then((res) => res.json());
if (!post) {
return { notFound: true };
}
return {
props: {
post,
},
revalidate: 60,
};
};
getStaticPropsを使用してプレビューモードが有効な場合に限り、draftKeyを用いてmicroCMSから下書き状態の記事データを取得します。また、getStaticPathsでは、fallbackをblockingに設定しています。これにより、ビルド時に生成されていないパスへのアクセスがあった場合、Next.jsがサーバーサイドでページを生成し、クライアントへ提供するまでリクエストをブロックします。この挙動は、プレビュー機能を利用する際に特に有用だと思います。
プレビューモードの終了とCookieの削除
プレビューモードを終了し、関連するCookieをクリアするには、CookieをクリアするためのAPI Routeを設定します。このAPI Routeは、プレビューデータをクリアすることでプレビューモードを無効化し、ユーザーが通常のコンテンツ閲覧状態に戻すことができます。
処理の流れ(シーケンス図)
実装例
例えば、Next.jsのAPI Routesを使用して/api/clear-preview
というエンドポイントを作成し、以下のような処理を行います。
Cookieクリア用のAPI route
src/pages/api/exit-preview.ts
import type { NextApiHandler } from 'next';
const exitPreview: NextApiHandler = (req, res) => {
// プレビューモードのCookieをクリア
res.clearPreviewData();
res.writeHead(307, { Location: '/' });
res.end();
};
export default exitPreview;
そして、このAPI Routeを呼び出すためのボタンを、ウェブページ上に設置します。
Cookieクリア用のAPIを呼び出すボタン
ExitPreviewButton.tsx
import { useRouter } from 'next/router';
import React from 'react';
export const ExitPreviewButton = () => {
const router = useRouter();
const exitPreviewMode = async () => {
const res = await fetch('/api/exit-preview');
if (res.ok) router.push('/');
};
return (
<button
onClick={exitPreviewMode}
style={{
position: 'fixed',
bottom: '10px',
right: '10px',
zIndex: 1,
cursor: 'pointer',
padding: '10px 15px',
backgroundColor: 'black',
color: 'white',
border: 'none',
borderRadius: '5px',
fontSize: '14px',
}}
>
プレビューモードを終了
</button>
);
};
このようにして、プレビューモードの終了とCookieの削除を行うことができます。
プレビューモードの表示管理
記事ページ上に「現在プレビュー中です」というメッセージを表示させるとコンテンツ編集者がプレビューモードであることを認識できます。あるとわかりやすいので、実装するといいかもしれません(必須じゃない)。
プレビューモードの表示UI
import React from 'react';
export const PreviewModeBanner = () => {
return (
<div
style={{
position: 'fixed',
top: '0px',
zIndex: 1,
width: '100%',
textAlign: 'center',
padding: '4px',
backgroundColor: 'black',
fontSize: '12px',
color: 'white',
}}
>
プレビューモード
</div>
);
};
Bannerって変かな?
さいごに
Next.jsのPreview Modeのおかげで、実装自体は簡単でしたが 「現在どのプロセスを実行中で、どのシステムやサービスに対してどんな操作を行っているのか?」 の把握に少し時間がかかりました。
なので、プレビュー機能を実装する過程を分けて解説し、図解した上で今はどの過程かを示して書きました。この記事が参考になれば嬉しいです。
参考
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion