microCMS

microCMSとNext.js13 Server Components

森茂洋

この記事は公開後、1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに


こんにちは、フロントエンドエンジニアの森茂です。

Next.js v13からapp directoryが利用可能になりました。2022年12月現在、まだbeta版ではありますが、Next.jsのあたらしい方向性を垣間見ることができます。またapp directoryではデフォルトでReact Server Componentが採用されているのも大きな特徴です。

もちろんmicroCMSはServer Componentでも利用ができます。今回はapp directoryを利用する環境でのmicroCMSの利用方法についていくつか紹介します。

更新情報

microcms-js-sdk v2.5.0からcustomRequestInitとしてfetchへリクエストオプションを追加できるようになりました。
Next.jsApp Routerで利用されるfetchcacheオプションを付与することも可能になっています。
以下の記事も参照ください。
microcms-js-sdkでfetchリクエストオプションが追加できるようになりました

環境について


  • Node.js 16.8.1
  • Next.js 13.1


※ 2022年12月現在Next.js 13のapp directoryはbeta版となります。今後のバージョンアップによっては実装が変更になる可能性がある点あらかじめご了承ください。

Next.js 13 app directoryについて


app directoryに関連するNext.js 13のbeta機能についてはbetaドキュメントも参考にしてください。改めて従来のレンダリング方式、SG、SSR、ISRについても復習しておくのがオススメです。


Next.js 13のセットアップ


まずはNext.js 13でapp directoryを利用するためのセットアップアップをします。app directoryは新規プロジェクト作成時にexperimental-appフラグを利用することであらかじめ公式に用意されたテンプレートが利用できます。

既存環境からのapp directoryへの移行については公式のUpgrade Guideを参照ください。

npx create-next-app@latest --experimental-app nextjs13-microcms


インストール、セットアップができたところで一度起動してみましょう。

npm run dev


問題なく起動できたらhttp://localhost:3000にてテストページを確認しておきましょう。

microCMS環境の準備


microCMSでAPIを用意します。今回はmicroCMSのブログテンプレートを利用して進めていきます。



アカウント登録がまだの方はこちらのドキュメントを参考に、登録を行ってください。


また、APIへの接続を簡単に行えるようmicrocms-js-sdkをインストールします。

npm install microcms-js-sdk


APIキーとサービスドメイン


あわせてAPIのエンドポイントと、APIキーも用意しておきましょう。ブログ一覧画面のAPIプレビューから確認するのがレスポンスの中身も確認できるので手軽で便利です。



取得したAPIキーとAPIのサービスドメイン名は環境変数ファイル.env.localに記載しておきます。

MICROCMS_SERVICE_DOMAIN=サービスドメイン名
MICROCMS_API_KEY=APIキー


コンテンツ取得用クライアントの作成


準備ができたところでNext.jsからmicroCMSのコンテンツを取得するためのクライアントを作成します。

libs/microcms.tsファイルとして型定義、クライアント、ブログ一覧の取得、ブログ詳細の取得を用意しておきます。

microcms-js-sdkにはMicroCMSQueriesMicroCMSImageMicroCMSDateなどAPIで利用する定形の型があらかじめ用意されています。こちらもぜひご活用ください。

// libs/microcms.ts

import { createClient } from "microcms-js-sdk";
import type {
 MicroCMSQueries,
 MicroCMSImage,
 MicroCMSDate,
} from "microcms-js-sdk";

//ブログの型定義
export type Blog = {
 id: string;
 title: string;
 content: string;
 eyecatch?: MicroCMSImage;
} & MicroCMSDate;

if (!process.env.MICROCMS_SERVICE_DOMAIN) {
 throw new Error("MICROCMS_SERVICE_DOMAIN is required");
}

if (!process.env.MICROCMS_API_KEY) {
 throw new Error("MICROCMS_API_KEY is required");
}

// API取得用のクライアントを作成
export const client = createClient({
 serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
 apiKey: process.env.MICROCMS_API_KEY,
});

// ブログ一覧を取得
export const getList = async (queries?: MicroCMSQueries) => {
 const listData = await client.getList<Blog>({
  endpoint: "blogs",
  queries,
 });

 // データの取得が目視しやすいよう明示的に遅延効果を追加
 await new Promise((resolve) => setTimeout(resolve, 3000));

 return listData;
};

// ブログの詳細を取得
export const getDetail = async (
 contentId: string,
 queries?: MicroCMSQueries
) => {
 const detailData = await client.getListDetail<Blog>({
  endpoint: "blogs",
  contentId,
  queries,
 });

 // データの取得が目視しやすいよう明示的に遅延効果を追加
 await new Promise((resolve) => setTimeout(resolve, 3000));

 return detailData;
};


今回はServer Componentsでの利用を想定しているため、サーバーサイドでのみ利用可能な環境変数を使っています。microcms-js-sdkをClient Componentsでも利用する場合は環境変数NEXT_PUBLIC_*を利用する必要があります。


また、今回はデータの取得がブラウザ上で目視しやすいよう明示的に3秒の遅延効果を追加しています。

ページの作成


準備ができたところで早速ページを作成していきましょう。

Server Components - Static Rendering


Next.js 13のServer ComponentsにはStaticレンダリングとDynamicレンダリングがあり、デフォルトではStaticレンダリングとなります。ビルド時にレンダリングされるため従来のgetStaticPropsを利用した挙動と同じようなものになります。


記事一覧ページ


まずはStaticレンダリングを想定した記事一覧ページを作成します。app/static/page.tsxファイルを新規に作成します。今回はページの生成タイミングを見たいので生成された時間も出力されるようにしておきます。

// app/static/page.tsx

import Link from "next/link";
import { getList } from "../../libs/microcms";

export default async function StaticPage() {
 const { contents } = await getList();

 // ページの生成された時間を取得
 const time = new Date().toLocaleString();

 if (!contents || contents.length === 0) {
  return <h1>No contents</h1>;
 }

 return (
  <div>
   <h1>{time}</h1>
   <ul>
    {contents.map((post) => {
     return (
      <li key={post.id}>
       <Link href={`/static/${post.id}`}>{post.title}</Link>
      </li>
     );
    })}
   </ul>
  </div>
 );
}


記事詳細ページ



また、コンテンツ部分のHTMLをパースするためhtml-react-parserをインストールします。

npm install html-react-parser


新規にstatic/[postId]/page.tsxファイルを作成します。

// static/[postId]/page.tsx

import { notFound } from "next/navigation";
import parse from "html-react-parser";
import { getDetail, getList } from "../../../libs/microcms";

export async function generateStaticParams() {
 const { contents } = await getList();

 const paths = contents.map((post) => {
  return {
   postId: post.id,
  };
 });

 return [...paths];
}

export default async function StaticDetailPage({
 params: { postId },
}: {
 params: { postId: string };
}) {
 const post = await getDetail(postId);

 // ページの生成された時間を取得
 const time = new Date().toLocaleString();

 if (!post) {
  notFound();
 }

 return (
  <div>
   <h1>{post.title}</h1>
   <h2>{time}</h2>
   <div>{parse(post.content)}</div>
  </div>
 );
}


動作の確認


npm run devを利用した開発サーバーでは常にレンダリングされてしまうため、npm run buildにて一度ビルドしてから動作を確認します。

npm run build
npm run start


ビルド時に/staticページがStaticとして生成されているのが確認できると思います。

├ ○ /static
├ ● /static/[postId]
├  └ /static/35yo7qzs1e8

○ (Static) automatically rendered as static HTML (uses no initial props)




http://localhost:3000/staticをブラウザで確認してみましょう。ブログのタイトルと共にビルドされた時間が表示され、ブラウザを更新しても同じ日時のままになっているはずです。

Server Components - Dynamic Rendering


Dynamicレンダリングでは従来のgetServerSidePropsの挙動とgetStaticProps内でrevalidateを利用したISRと同じ動きを利用することができます。

Next.jsに組み込まれている拡張されたfetchではオプションを渡すことでキャッシュの制御が可能ですが、microcms-js-sdkを利用の場合は、export const revalidate = 60といった形でオプションを明示的に記載する必要があります。


export const revalidate = 60とすれば60秒間はキャッシュを利用するISRに、export const revalidate = 0とすれば常にレンダリングを行うSSRという動きになります。

記事一覧ページ


app/dynamic/page.tsxを新規に作成して動作を試してみましょう。キャッシュの設定以外はapps/static/page.tsxと同様です。

// app/dynamic/page.tsx

import Link from "next/link";
import { getList } from "../../libs/microcms";

// キャッシュを利用しない
export const revalidate = 0;

export default async function StaticPage() {
 const { contents } = await getList();

 // ページの生成された時間を取得
 const time = new Date().toLocaleString();

 if (!contents || contents.length === 0) {
  return <h1>No contents</h1>;
 }

 return (
  <div>
   <h1>{time}</h1>
   <ul>
    {contents.map((post) => {
     return (
      <li key={post.id}>
       <Link href={`/dynamic/${post.id}`}>{post.title}</Link>
      </li>
     );
    })}
   </ul>
  </div>
 );
}


記事詳細ページ


app/dynamic/[postId]/page.tsxを作成します。キャッシュの設定以外はこちらもStaticなページと同じ形になっています。

// app/dynamic/[postId]/page.tsx

import { notFound } from "next/navigation";
import parse from "html-react-parser";
import { getDetail, getList } from "../../../libs/microcms";

// キャッシュを利用しない
export const revalidate = 0;

export async function generateStaticParams() {
 const { contents } = await getList();

 const paths = contents.map((post) => {
  return {
   postId: post.id,
  };
 });

 return [...paths];
}

export default async function StaticDetailPage({
 params: { postId },
}: {
 params: { postId: string };
}) {
 const post = await getDetail(postId);

 // ページの生成された時間を取得
 const time = new Date().toLocaleString();

 if (!post) {
  notFound();
 }

 return (
  <div>
   <h1>{post.title}</h1>
   <h2>{time}</h2>
   <div>{parse(post.content)}</div>
  </div>
 );
}


動作の確認


こちらもビルドして確認します。

npm run build
npm run start


/dynamicのページはSSRとしてビルドされているのが確認できます。

├ λ /dynamic
├ ● /dynamic/[postId]
├  └ /dynamic/35yo7qzs1e8

λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)


さらにブラウザでhttp://localhost:3000/dynamicを確認してみると、Staticのページと異なりページの更新ごとに日時が変わるのを確認できるはずです。またデータの取得には3秒の遅延処理を入れているのでページが表示されるまで時間がかかります。


ISR - revalidateの動作


あわせてrevalidateの値を0から60に変更してビルドし直します。最初のデータ取得後60秒間はキャッシュが表示され、60秒経過後のアクセスでは古いキャッシュを表示しながら、サーバーサイドではキャッシュを更新し、次のアクセスからは再取得した新しいキャッシュを利用して表示を行ってくれます。

//...

// 60秒間はキャッシュを利用する
export const revalidate = 60;

//...


このような方法でmicroCMSのコンテンツを配信用途にあわせて利用することができます。

Server Componentsの活用方法


ここからは、公式betaドキュメントからServer ComponentsでmicroCMSを利用する際に役立ちそうな機能を一部紹介します。

Loading UI


app directoryの構成では同じディレクトリ下にloading.jsを置くことでpage.jsのレンダリング完了までローディング画面を出力することが可能になっています。

// app/dynamic/loading.tsx

export default function Loading() {
 return <h1>Loading...</h1>;
}



サーバーサイドでは先にloading.tsxがレンダリングされブラウザに返されます。その後ページ(page.tsx)のレンダリング情報はストリーム配信され、最終的なページが表示されます。

一般的なサーバーサイドでのレンダリングだと処理に時間がかかる場合、レンダリングが完了するまでユーザーにローディング情報すら渡すことができませんが、取得や処理に時間のかかる部分を遅延して配信することでユーザー体験の向上につなげることが可能です。

公式ドキュメントではスケルトンスクリーンUIの実装例も紹介されています。


Suspenseを利用した部分的なストリーム配信


Suspenseを利用した部分的なストリーム配信も活用範囲は広そうです。


新規にapp/suspense/page.tsxapp/suspense/blog.tsxapp/suspense/blog-props.tsxapp/suspense/blog-use.tsxを作成します。データの取得をどの部分で行うかによって少し扱い方が異なりますが、以下では非同期コンポーネントでの取得、PromiseをPropsで渡す場合、React.useを利用する場合の3パターンを用意してみます。

// app/suspense/page.tsx

import { Suspense } from "react";
import { getList } from "../../libs/microcms";
import { Blog } from "./blog";
import { BlogPromiseProps } from "./blog-props";
import { BlogUse } from "./blog-use";


// キャッシュを利用しない
export const revalidate = 0;


export default async function StaticPage() {
  const data = getList();


  // ページの生成された時間を取得
  const time = new Date().toLocaleString();


  if (!data) {
    return <h1>No contents</h1>;
  }


  return (
    <div>
      <h1>{time}</h1>
      <h2>非同期コンポーネント</h2>
      <Suspense fallback={<div>loading...</div>}>
        {/* @ts-expect-error Server Component */}
        <Blog />
      </Suspense>
      <hr />
      <h2>PromiseをPropsで渡す場合</h2>
      <Suspense fallback={<div>loading...</div>}>
        {/* @ts-expect-error Server Component */}
        <BlogPromiseProps promise={data} />
      </Suspense>
      <hr />
      <h2>React.use()を利用</h2>
      <Suspense fallback={<div>loading...</div>}>
        <BlogUse />
      </Suspense>
    </div>
  );
}


asyncを利用した非同期コンポーネントはJSX上でType Errorが出るため現時点では{/* @ts-expect-error Server Component */}でエラーを回避する必要があります。

非同期コンポーネント

// app/suspense/blog.tsx

import { getList } from "../../libs/microcms";

export async function Blog() {
 const { contents } = await getList();

 if (!contents || contents.length === 0) {
  return <h1>No contents</h1>;
 }

 return (
  <ul>
   {contents.map((item) => {
    return (
     <li key={item.id}>
      <h1>{item.title}</h1>
     </li>
    );
   })}
  </ul>
 );
}

PromiseをPropsで渡す

// app/suspense/blog-props.tsx

import { BlogResponse } from "../../libs/microcms";

export async function BlogPromiseProps({
 promise,
}: {
 promise: Promise<BlogResponse>;
}) {
 const { contents } = await promise;

 return (
  <ul>
   {contents.map((item) => {
    return (
     <li key={item.id}>
      <h1>{item.title}</h1>
     </li>
    );
   })}
  </ul>
 );
}

React.useを利用する

// app/suspense/blog-use.tsx

import { use } from "react";
import { getList } from "../../libs/microcms";


export function BlogUse() {
  const { contents } = use(getList());


  if (!contents || contents.length === 0) {
    return <h1>No contents</h1>;
  }


  return (
    <ul>
      {contents.map((item) => {
        return (
          <li key={item.id}>
            <h1>{item.title}</h1>
          </li>
        );
      })}
    </ul>
  );
}


ビルド後にhttp://localhost:3000/suspenseを確認するとそれぞれのコンポーネントがページレンダリング後にストリーム配信されることが確認できます。


さいごに


Next.js 13のapp directoryでのmicroCMSの利用例について紹介させていただきました。ほんの一部の機能だけですが構築の仕方はがらっと変わりそうです。まだbeta版のためプロダクション環境での利用は難しいかもしれませんが、ぜひapp directoryの素振りがてらmicroCMSとの連携を試してみてください。

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

森茂洋
Web制作、開発会社を経て2022年11月にmicroCMSにジョイン。きっとインターネット老人会に所属しています。インフラやWebに関わる技術の探訪が大好きで興味をもった技術は広く深く掘り下げていくことが信念。microCMSではフロントエンドテックリードとして開発チームのサポートを担当。趣味はサイクリングとアイスホッケー、そして甘いもの。