microCMSを使って、Next.jsのDraft Modeを学ぶ

## はじめに
Next.jsの[Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode)と呼ばれる手法を使えば、ヘッドレスCMSの下書きコンテンツを公開されているアプリケーションで確認することができます。

この記事では[microCMS](https://microcms.io/)を使って、Draft Modeを使った機能を実装します。

コードの全貌はGitHubで確認できます。

https://github.com/k35o/draft-mode

:::note warn
microCMSの使用はこれが初めてなので、一部不要な作業を行なっている恐れがあります
:::

## Next.jsの環境作成
今回利用するNext.jsの環境についてこだわる箇所がないので、自動インストールで環境を準備します。
```command
npx create-next-app@latest
```
![スクリーンショット 2024-12-19 16.18.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/18fad0af-a922-4f32-fa46-4745196764cb.png)

その後、`favicon.ico`の削除などNext.jsの要素を削除します。
```css:app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

```tsx:app/layout.tsx
import type { Metadata } from "next";
import { Noto_Sans_JP } from "next/font/google";
import "./globals.css";

const notoSansJP = Noto_Sans_JP({
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Draft Mode Example",
  description: "Example of using draft mode in Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">
      <body className={notoSansJP.className}>
        {children}
      </body>
    </html>
  );
}
```

```tsx:app/page.tsx
export default function Home() {
  return (
    <main className="flex flex-col items-center justify-center w-screen h-screen">
      <section className="rounded-lg shadow-lg p-4 flex flex-col gap-4 bg-slate-300">
        <h1 className="text-lg font-bold">お知らせ</h1>
        <ul>
          <li>
            <div className="flex justify-between gap-2 items-end">
              <p>ファンタスティックな機能を追加しました</p>
              <p className="text-sm text-gray-800">2021-01-03</p>
            </div>
          </li>
          <li>
            <div className="flex justify-between gap-2 items-end">
              <p>お知らせのページを追加しました</p>
              <p className="text-sm text-gray-800">2021-01-02</p>
            </div>
          </li>
          <li>
            <div className="flex justify-between gap-2 items-end">
              <p>サイトを公開しました</p>
              <p className="text-sm text-gray-800">2021-01-01</p>
            </div>
          </li>
        </ul>
      </section>
    </main>
  );
}
```
`app/page.tsx`はmicroCMSから取得したお知らせを表示したいので、簡単に見た目を作っています。

## microCMSの環境作成
まずは、アカウントの作成とサービスの作成を行います。
![スクリーンショット 2024-12-19 16.20.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/76b3171b-0829-28de-fd75-98d939cdbf86.png)

次に、APIテンプレートから「お知らせ」を作成します。
![スクリーンショット 2024-12-19 18.31.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/e79f42ae-922f-fd59-ce46-11d26cb0e9a9.png)

自動でお知らせが一つ作成されているはずです。それをNext.jsで確認するために、次はお知らせの一覧を取得する処理を記述します。
今回は簡単にデータを表示させるだけなので`fetch`を用いてデータを取得します。`fetch`の他にも[sdk](https://github.com/microcmsio/microcms-js-sdk)からデータを取得することも可能なようです。

`.env`等に`MICROCMS_API_KEY`を登録すると以下の`getData`でお知らせの一覧を取得できます。
```tsx
type News = {
  contents: {
    id: string;
    title: string;
    publishedAt?: string;
  }[];
  totalCount: number;
  offset: number;
  limit: number;
}

async function getData(): Promise<News> {
  const url = 'https://draft-mode.microcms.io/api/v1/news';

  const res = await fetch(url, {
    headers: {
      'X-MICROCMS-API-KEY': process.env.MICROCMS_API_KEY ?? '',
    },
  });

  return res.json();
}

const dtf = new Intl.DateTimeFormat('ja-JP', {
  year: 'numeric',
  month: 'numeric',
  day: 'numeric',
});

export default async function Home() {
  const { contents } = await getData();
  return (
    <main className="flex flex-col items-center justify-center w-screen h-screen">
      <section className="rounded-lg shadow-lg p-4 flex flex-col gap-4 bg-slate-300">
        <h1 className="text-lg font-bold">お知らせ</h1>
        <ul>
          {contents.map((content) => (
            <li key={content.id}>
              <div className="flex justify-between gap-2 items-end">
                <p>{content.title}</p>
                <p className="text-sm text-gray-800">
                  {content.publishedAt
                    ? dtf.format(new Date(content.publishedAt))
                    : 'draft'}
                </p>
              </div>
            </li>
          ))}
        </ul>
      </section>
    </main>
  );
}
```
次に下書き状態のお知らせを作ります。

![スクリーンショット 2024-12-19 18.45.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/7029257c-6176-5620-dd90-5ee6404dec8f.png)

下書きのお知らせはそれぞれが`draftKey`を持っており、それを`fetch`のリクエスト時にパラメータとして含めると下書き状態のお知らせも併せて取得できるようです。`draftKey`はお知らせの詳細ページの左上に書かれています。
![スクリーンショット 2024-12-19 18.47.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/69f1fcb0-7a48-7185-631f-f1d6c7c259d8.png)

```ts
async function getData(): Promise<News> {
  const url = 'https://draft-mode.microcms.io/api/v1/news';

  const res = await fetch(`${url}?draftKey=D9No73sxg5`, {
    headers: {
      'X-MICROCMS-API-KEY': process.env.MICROCMS_API_KEY ?? '',
    },
  });

  return res.json();
}
```
先ほどの`getData`をこれに変更すると、下書き状態のお知らせも一覧に出てきたはずです。

## Draft Mode
準備が完了しました。Draft Modeを利用して、特定のリクエストを送った時だけ指定した下書きのお知らせを含んだUIを表示させます。

まずは、Router Handlerで特定のリクエストを行う宛先を作成します。
```ts:app/api/draft-news/route.ts
import { draftMode } from 'next/headers'
 
export async function GET() {
  const draft = await draftMode()
  draft.enable()
  return new Response('Draft mode is enabled')
}
```
Draft Modeを有効にしてテキストレスポンスを返すだけのAPIです。

このAPIにアクセスしてみると、レスポンスヘッダーの`Set-Cookie`に`__prerender_bypass`が含まれています。
![スクリーンショット 2024-12-19 19.06.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/f6efb923-dcb5-3429-a923-b821298fedb5.png)
これがあることで後続の処理でNext.jsはDraft Modeが有効となっていることを判別します。
通常のレスポンスには含まれないことは`draft.enbale()`を消すとわかります。

次に、このAPIに`draftKey`と`secret`を受け取って、正常であれば`/`にリダイレクトするような処理を追加します。

```ts
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 draftKey = searchParams.get('draftKey')

  if (secret !== process.env.MICROCMS_API_KEY || !draftKey) {
    return new Response('Invalid token', { status: 401 })
  }

  const draft = await draftMode()
  draft.enable()

  redirect('/?draftKey=' + draftKey)
}
```
`secret`を受け取って検証するようにしたのは、`draftKey`を適当に入力して下書き状態のお知らせを閲覧されることを防ぐためです。

`/`に`draftKey`が渡ってくるようになりました。Draft Modeが有効なときは`draftKey`の下書きも表示するようにしましょう。
```tsx:app/page.tsx
import { draftMode } from "next/headers";

type News = {
  contents: {
    id: string;
    title: string;
    publishedAt: string;
  }[];
  totalCount: number;
  offset: number;
  limit: number;
}

async function getData({ draftKey }: { draftKey?: string }): Promise<News> {
  const { isEnabled } = await draftMode()

  const url = isEnabled
    ?'https://draft-mode.microcms.io/api/v1/news?draftKey=' + draftKey
    : 'https://draft-mode.microcms.io/api/v1/news';

  const res = await fetch(url, {
    headers: {
      'X-MICROCMS-API-KEY': process.env.MICROCMS_API_KEY ?? '',
    },
  });

  return res.json();
}

const dtf = new Intl.DateTimeFormat('ja-JP', {
  year: 'numeric',
  month: 'numeric',
  day: 'numeric',
});

export default async function Home({
  searchParams,
}: {
  searchParams: Promise<{
    draftKey: string | undefined;
  }>;
}) {
  const draftKey = (await searchParams).draftKey;
  const { contents } = await getData({ draftKey });
  return (
    <main className="flex flex-col items-center justify-center w-screen h-screen">
      <section className="rounded-lg shadow-lg p-4 flex flex-col gap-4 bg-slate-300">
        <h1 className="text-lg font-bold">お知らせ</h1>
        <ul>
          {contents.map((content) => (
            <li key={content.id}>
              <div className="flex justify-between gap-2 items-end">
                <p>{content.title}</p>
                <p className="text-sm text-gray-800">
                  {content.publishedAt
                    ? dtf.format(new Date(content.publishedAt))
                    : 'draft'}
                </p>
              </div>
            </li>
          ))}
        </ul>
      </section>
    </main>
  );
}
```
Draft Modeが有効であることは`isEnabled`で判断します。`isEnabled`が`true`な時にだけ受け取った`draftKey`を含めたリクエストをmicroCMSに送信します。
`app/api/draft-news/route.ts`で`draft.enable()`を削除したときは動作しないことを安全のため確認してください。

これによって通常は投稿済みのお知らせしか表示されないが、お知らせを管理する人が限定的にmicroCMSのAPIキーと`draftKey`を用いて下書きのお知らせを含めた表示を確認する仕組みができました。

## おわりに
私はCMSを使った経験がなかったので、ドキュメントを読むだけではイマイチ理解を進められませんでした。
実際に動かしてみることで、Draft Modeがどのようなものか、どのようなところが嬉しいのかが理解できてよかったです。
開発者とCMSの管理者が別である場合、お知らせを投稿する前の見た目の調整は大変なはずなので、この機能があるとかなり業務の効率化が図られるのではないでしょうか。