Kitsune Blog

Next.jsのApp RouterでContentfulのlive preview modeを使用する。

当ブログはNext.jsのApp Routerを使用していますが、こちらにContentfulのlive preview modeを導入したので、メモ書きを。

※当ブログではContentfulの投稿データを取得するためにcreateClientを利用しているため、createClientを用いた方法について記述いたします。

まず、contentfulの管理画面から右上のSettings→Content previewと進みます。

「Create preview platform」を押下し、入力欄を入力します。

「Preview platform name」 は必須のため、previewと入力しました。

Preview URLはサイトのURLと/api/draft?secret=<CONTENTFUL_PREVIEW_SECRET>&slug={entry.fields.slug} をつなげたものを入力します。

https://your-blog.vercel.app/api/draft?secret=<CONTENTFUL_PREVIEW_SECRET>&slug={entry.fields.slug}

CONTENTFUL_PREVIEW_SECRET に設定した値は、Next.jsの環境変数に入れておく必要があります。

その後Contentful Live Preview SDK (https://github.com/contentful/live-preview)をインストールします。

npm install @contentful/live-preview

Next.jsの Draft Mode を利用し、プレビュー用の Route Handlers を作成し、プレビューデータを取得できるようにします。 Preview URLで指定したURLが対応します。api/draft/route.tsにファイルを作成し、下記のように記述します。

import { createClient } from "contentful";
import { cookies, draftMode } from "next/headers";
import { redirect } from "next/navigation";

import { IBlogFields } from "../../../@types/generated/contentful";

export const dynamic = "force-dynamic";

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

  if (!secret || !slug) {
    return new Response("Missing parameters", { status: 400 });
  }

  if (secret !== process.env.CONTENTFUL_PREVIEW_SECRET) {
    return new Response("Invalid token", { status: 401 });
  }

  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID as string,
    accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN as string,
    host: "preview.contentful.com",
  });
  const res = await client.getEntries<IBlogFields>({
    content_type: "blog",
    "fields.slug": slug,
  });
  const blog = res.items[0];

  if (!blog) {
    return new Response("Blog not found", { status: 404 });
  }

  draftMode().enable();

  // This is a hack due to a bug with cookies and NextJS, this code might not be required in the future
  const cookieStore = cookies();
  const cookie = cookieStore.get("__prerender_bypass");
  cookies().set({
    name: "__prerender_bypass",
    value: cookie?.value || "",
    httpOnly: true,
    path: "/",
    secure: true,
    sameSite: "none",
  });

  redirect(`/post/${slug}`);
}

Next.js側のバグもあり、一部ハックのような書き方も含まれますが、この部分でCONTENTFUL_PREVIEW_TOKEN を利用し、下書きの投稿データが存在するかどうか確かめています。

  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID as string,
    accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN as string,
    host: "preview.contentful.com",
  }); 

Draft Modeが有効だという情報をcookieにつめて、その後投稿ページにリダイレクトしています。

 draftMode().enable(); 
 redirect(`/post/${slug}`);

ContentfulLivePreviewProvider を使用するためのProviderコンポーネントを作成します。

"use client";

import { ContentfulLivePreviewProvider } from "@contentful/live-preview/react";

export const LivePreviewProvider = ({
  isEnabled,
  children,
}: {
  isEnabled: boolean;
  children: React.ReactNode;
}) => {
  return (
    <ContentfulLivePreviewProvider
      locale="ja-JP"
      enableInspectorMode={isEnabled}
      enableLiveUpdates={isEnabled}
      debugMode={isEnabled}
    >
      {children}
    </ContentfulLivePreviewProvider>
  );
};

大元のlayout.tsxでLivePreviewProvider を下記のように使用します。

import "styles/globals.css";

import { LivePreviewProvider } from "components/LIvePreviewProvider";
import { draftMode } from "next/headers";
import { ReactNode } from "react";

export default function RootLayout({ children }: { children: ReactNode }) {
  const { isEnabled } = draftMode();
  return (
    <html lang="ja">
      <body>
        <LivePreviewProvider isEnabled={isEnabled}>
          {children}
        </LivePreviewProvider>
      </body>
    </html>
  );
}

cookieを参照してDraft Modeが有効か否かを判定しています。

const { isEnabled } = draftMode(); 

記事ページで下記のように実装。

Draft Modeが有効か否かで取得する投稿データが異なるため、createClient のパラメーターのaccessTokenとhostをisEnabled を参照して分岐する必要があります。

const Page = async ({ params }: { params: { slug: string } }) => {
  const { isEnabled } = draftMode();
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID as string,
    accessToken: isEnabled
      ? (process.env.CONTENTFUL_PREVIEW_TOKEN as string)
      : (process.env.CONTENTFUL_ACCESS_TOKEN as string),
    host: isEnabled ? "preview.contentful.com" : "cdn.contentful.com",
  });
  const res = await client.getEntries<IBlogFields>({
    content_type: "blog",
    "fields.slug": params.slug,
  });
  const blog = res.items[0];
  if (!blog) return notFound();

  return <Contents blog={blog} />;
};

export default Page;

子コンポーネントのContentsでは、下記の書き方で下書き中のものを即時反映した投稿データを取得することができます。

livePostに入っている投稿データを記事ページで出力するように実装すると本番環境はそのままで、live preview modeで下書き記事を即時に確認できる実装となります。

"use client";

import { useContentfulLiveUpdates } from "@contentful/live-preview/react";

export const Contents = ({ blog }: { blog: Entry<IBlogFields> }) => {
  const livePost = useContentfulLiveUpdates(blog);
  ..........

管理画面の記事作成画面から、右側の「Open Live Preview」を押下すると、live preview modeで記事を作成できます。下記URLからlive preview modeの使用感を確認できます。

https://kittsun.net/live-preview-sample.mp4

参考記事

https://www.jondjones.com/learn-contentful-cms/getting-started/how-to-set-up-live-preview-with-contentful-and-nextjs/

https://www.newt.so/docs/tutorials/nextjs-preview-mode

参考動画

https://www.youtube.com/watch?v=TC-xD9jTm6g