はじめに
こんにちは、フロントエンドエンジニアの森茂です。
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.jsのApp Routerで利用されるfetchのcacheオプションを付与することも可能になっています。
以下の記事も参照ください。
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にはMicroCMSQueries
、MicroCMSImage
やMicroCMSDate
など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.tsx
、app/suspense/blog.tsx
、app/suspense/blog-props.tsx
、app/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との連携を試してみてください。