はじめに
ブログサイトを作る過程で学んだことを、備忘録目的で投稿しています。
自身は駆け出しエンジニアであり、React自体がほぼ初学者のため、誤った認識・理解をしている可能性があります。
万が一参考にする場合は、上記の点を考慮した上でご一読ください。
また、スタイリングについては割愛しています。
目的
Next.jsを利用し、SSG(Static Site Generation)を実現したブログサイトを作成する。
記事投稿にはmicroCMSを採用する。
ホスティングサービスはVercelを利用し、記事投稿に合わせて自動でデプロイし、サイト表示が更新されるようにする。
作業環境
Windows10 Pro x64
Node.js: 20.14.0
npm: 10.3.0
Next.js: 14.2.3(App Router使用)
手順
1. プロジェクトの準備
Next.jsのインストール
下記コマンドを実行して自動セットアップを行う。
npx create-next-app@latest
√ What is your project named? ... project_name
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like to use `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to customize the default import alias (@/*)? ... No / Yes
今回は上から順に下記のように設定しました。
- プロジェクト名 → 任意の名前を入力
- TypeScriptの使用 → No
- ESLintの使用 → Yes
- Tailwind CSSの使用 → No
-
src
ディレクトの使用 → Yes - の使用 → No
インストール完了後は npm run dev
コマンドで http://localhost:3000 の開発サーバーを立ち上げて確認します。
Sassのインストール、JSXに変更
npm i -D sass
を実行してSass
をインストールし、その後css
ファイルをscss
ファイルに変更します。
また、layout.js
、page.js
をそれぞれjsx
ファイルに変更します。
CSS modulesを採用した構成にしています。
2. ページの作成
まずは基本となる骨格を作成しました。
各JSXファイルは下記のようにしました。
Next.jsでは画像にnext/image
、リンクにはnext/link
を使用することでパフォーマンスが向上します。
import "./globals.scss";
export const metadata = {
title: "",
description: "",
};
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>{children}</body>
</html>
);
}
import Image from "next/image";
import Link from "next/link";
import styles from "./page.module.scss";
export default function Home() {
return (
<main className={styles.main}>
<h1>記事一覧</h1>
<ul className={styles.cards}>
<li className={styles.card}>
<Link href="" className={styles.link}>
<p className={styles.thumbnail}>
<Image src="" alt="" height="" width="" />
</p>
<div>
<h2 className={styles.title}>記事タイトル01</h2>
<p>カテゴリ名</p>
</p>
</Link>
</li>
</ul>
</main>
);
}
importには@
から始まるエイリアスを使用し、importをわかりやすくしています。
エイリアス設定はjsconfig.json
で変更できます、今回は初期のままにしています。
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
次にこれらをコンポーネント化しました。
ディレクトリ構成を自分好みに変更、JSXファイルの記述も修正し、下記のようにしました。
下記のディレクトリ構成は変更を加えたもの、これからの説明に必要な箇所のみになります。
root/
├ src/
│ ├ app/
│ │ ├ globals.scss
│ │ ├ layout.jsx
│ │ ├ page.jsx
│ │ └ page.scss
│ │
│ ├ components/
│ │ ├ components/
│ │ │ ├ index.jsx
│ │ │ └ index.module.scss
│ │ │
│ │ └ components/
│ │ ├ index.jsx
│ │ └ index.module.scss
│ │
│ └ public/
└ jsconfig.json/
import styles from "./page.module.scss";
import { Cards } from "@/components/Cards";
export default function Home() {
return (
<main className={styles.main}>
<h1>記事一覧</h1>
<ul className={styles.cards}>
<Cards />
</ul>
</main>
);
}
import styles from "./index.module.scss";
import { Card } from "@/components/Card";
export const Cards = () => {
return (
<ul className={styles.cards}>
<Card />
</ul>
);
};
import Image from "next/image";
import Link from "next/link";
import styles from "./index.module.scss";
export const Card = () => {
return (
<li className={styles.card}>
<Link href="" className={styles.link}>
<p className={styles.thumbnail}>
<Image src="" alt="" height="" width="" />
</p>
<div>
<h2 className={styles.title}>記事タイトル01</h2>
<p>カテゴリ名</p>
</p>
</Link>
</li>
);
};
3. microCMSの準備
microCMSにアカウント登録します。
「一から作成する」を選択し、サービス情報を入力したらサービスを作成します。
早速APIを作成します。
今回はテンプレートの「ブログ」を使用します。
APIが作成できたので、自分好みに変更していきます。
右上の「API設定」をクリックし、下記のように変更しました。
▼基本情報
API名:ブログ → 記事
エンドポイント:blogs → articles
▼APIスキーマ
カテゴリ:category
タイトル:title
内容 → 本文:content → body
アイキャッチ → サムネイル画像:eyecatch → thumbnail
4. microCMSのAPIキーとサービスドメインを準備
APIへの接続を簡単に行えるようmicrocms-js-sdkをインストールします。
npm install microcms-js-sdk
microCMSのAPIキーを確認します、右上のAPIプレビューから確認できます。
.env.local
ファイルを作成し、microCMSのサービスドメイン名はとAPIキーを記載します。
サービスドメイン名はmicroCMSのURLでも確認でき、下記の箇所になります。
https://サービスドメイン名.microcms.io/
MICROCMS_SERVICE_DOMAIN=microCMSの登録したサービスドメイン名を入力
MICROCMS_API_KEY=microCMSのAPIキーを入力
5. コンテンツの取得
API取得用のファイルを準備
microCMSからコンテンツを取得するファイルを作成します。
libs
フォルダを作成し、その中にmicrocms.js
ファイルを作成して記述していきます。
下記のようにしました。
import { createClient } from "microcms-js-sdk";
import { notFound } from "next/navigation";
if (!process.env.MICROCMS_SERVICE_DOMAIN) {
throw new Error("MICROCMS_SERVICE_DOMAIN が設定されていません。");
}
if (!process.env.MICROCMS_API_KEY) {
throw new Error("MICROCMS_API_KEY が設定されていません。");
}
// API取得用のクライアントを作成
export const client = createClient({
serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
apiKey: process.env.MICROCMS_API_KEY,
});
// 記事一覧を取得
export const getArticlesList = async (queries) => {
try {
const response = await client.getList({
endpoint: "articles",
queries,
});
return response;
} catch (error) {
console.error("getArticlesListでエラーが発生しました => ", error);
notFound();
}
};
// 記事詳細を取得
export const getArticlesDetail = async (contentId, queries) => {
try {
const response = await client.getListDetail({
endpoint: "articles",
contentId,
queries,
});
return response;
} catch (error) {
console.error("getArticlesDetailでエラーが発生しました => ", error);
notFound();
}
};
解説
最初にmicrocms-js-sdk
の読み込み、そしてサービスドメイン、APIキーの設定を行います。
.env.local
にサービスドメイン、APIキーが設定されていない場合はエラーを返します。
notFound
はAPI取得エラーが発生した場合に、404エラーページを表示するために使用します。
import { createClient } from "microcms-js-sdk";
import { notFound } from "next/navigation";
if (!process.env.MICROCMS_SERVICE_DOMAIN) {
throw new Error("MICROCMS_SERVICE_DOMAIN が設定されていません。");
}
if (!process.env.MICROCMS_API_KEY) {
throw new Error("MICROCMS_API_KEY が設定されていません。");
}
// API取得用のクライアントを作成
export const client = createClient({
serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
apiKey: process.env.MICROCMS_API_KEY,
});
endpoint
にはmicroCMSに設定したエンドポイントを入力します、microCMSのAPI設定から確認できます。
queries
は、コンテンツを取得する際に絞り込みなどの機能を活用するために使用します。
コンテンツ取得エラーが発生した場合には、notFound
で404エラーページを表示するようにしています。
記事詳細のデータ取得には、contentId
を送信することで、必要な記事のみのデータを取得することができます。
// 記事一覧を取得
export const getArticlesList = async (queries) => {
try {
const response = await client.getList({
endpoint: "articles",
queries,
});
return response;
} catch (error) {
console.error("getArticlesListでエラーが発生しました => ", error);
notFound();
}
};
// 記事詳細を取得
export const getArticlesDetail = async (contentId, queries) => {
try {
const response = await client.getListDetail({
endpoint: "articles",
contentId,
queries,
});
return response;
} catch (error) {
console.error("getArticlesDetailでエラーが発生しました => ", error);
notFound();
}
};
コンテンツを取得
作成したgetArticlesList
関数を呼び出して、ブログ記事の一覧データを取得します。
レスポンスオブジェクトにはcontents
プロパティがあり、記事データが格納されています。
わかりやすいように、contents
をarticles
という名前に変更して使用します。
取得したデータをpropsで渡してあげます。
import { notFound } from "next/navigation";
import styles from "./page.module.scss";
import { Cards } from "@/components/Cards";
import { getArticlesList } from "@/libs/microcms";
export default async function Home() {
// ブログ一覧を取得
const articlesListResponse = await getArticlesList().catch(() => notFound());
const { contents: articles } = articlesListResponse;
return (
<main className={styles.main}>
<h1>記事一覧</h1>
<ul className={styles.cards}>
<Cards articles={articles} />
</ul>
</main>
);
}
import styles from "./index.module.scss";
import { Card } from "@/components/Card";
export const Cards = async ({ articles }) => {
return (
<ul className={styles.cards}>
{articles.map((article) => (
<Card article={article} key={article.id} />
))}
</ul>
);
};
import Image from "next/image";
import Link from "next/link";
import styles from "./index.module.scss";
export const Card = async ({ article }) => {
return (
<li className={styles.card}>
<Link href={`/articles/${article.id}`} className={styles.link}>
<p className={styles.thumbnail}>
<Image
src={article.thumbnail.url}
alt={article.title}
height={article.thumbnail.height}
width={article.thumbnail.width}
priority
/>
</p>
<div>
<h2 className={styles.title}>{article.title}</h2>
<p className={styles.category}>{article.category.name}</p>
</div>
</Link>
</li>
);
};
この状態で、npm run dev
を実行し、ページを表示してみると下記エラーが発生しました。
Error: Invalid src prop (https://images.microcms-assets.io/assets/a6a4f0e5be594b3c849be21f97efbe92/2cfd5d0adda9492190fc04404ac789fb/blog-template.png) on
next/image
, hostname "images.microcms-assets.io" is not configured under images in yournext.config.js
See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host
ChatGPTに対処法を教えてもらいました。
このエラーは、Next.jsの**
next/image
コンポーネントを使用しているときに発生します。next/image
は画像の最適化と提供を行うためのものですが、セキュリティとパフォーマンスの理由から、外部の画像ソースを使用する前に設定ファイル(next.config.js
**)でそのホスト名を明示的に許可する必要があります。エラーメッセージにある通り、**
https://images.microcms-assets.io
から画像を読み込もうとしていますが、このホスト名がnext.config.js
**に設定されていないため、エラーが発生しています。この問題を解決するには、**
next.config.js
ファイルを編集して、images
セクションにdomains
配列を追加し、その中に"images.microcms-assets.io"
**を含める必要があります。
next.config.js
をnext.config.mjs
に拡張子を変更し、内容を下記のように変更しました。
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.microcms-assets.io",
},
],
},
};
export default nextConfig;
再度、ページを確認すると無事データを取得できるようになりました。
詳細ページも作成します。
ディレクトリ構成を下記のようにし、詳細ページ用のpage.jsx
を作成します。
root/
├ src/
│ ├ app/
│ │ ├ articles/
│ │ │ └ [slug]/
│ │ │ ├ page.jsx
│ │ │ └ page.scss
│ │ │
│ │ ├ globals.scss
│ │ ├ layout.jsx
│ │ ├ page.jsx
│ │ └ page.scss
│ │
│ ├ components/
│ │ ├ components/
│ │ │ ├ index.jsx
│ │ │ └ index.module.scss
│ │ │
│ │ └ components/
│ │ ├ index.jsx
│ │ └ index.module.scss
│ │
│ ├ libs/
│ │ └ microcms.js
│ │
│ └ public/
│
├ env.local
├ jsconfig.json
└ next.config.mjs
import Image from "next/image";
import styles from "./page.module.scss";
import { getArticlesDetail } from "@/libs/microcms";
export default async function Page({ params }) {
// URLパラメータのIDを参照して、ブログの詳細を取得
const article = await getArticlesDetail(params.slug);
return (
<main className={styles.main}>
<div className={styles.article}>
<p className={styles.thumbnail}>
<Image
src={article.thumbnail.url}
alt={article.title}
height={article.thumbnail.height}
width={article.thumbnail.width}
priority
/>
</p>
<h1 className={styles.title}>{article.title}</h1>
<p className={styles.category}>{article.category.name}</p>
<div className={styles.body}>{article.body}</div>
</div>
</main>
);
}
params
はNext.jsの機能の一部であり、ルーティング時にURLパラメータを取得するために使用されます。
試しにparams
の中身がどうなっているのがコンソールに表示してみると下記の通りになっています。
console.log("params => ", params);
// 出力結果
params => { slug: 'yy3_bxzq2f-0' }
データを取得できましたが、本文にHTML文字列がそのまま表示されてしまっています。
これを解決するためには、html-react-parser
をインストールして使用します。
html-react-parser
は、HTML文字列をReactコンポーネントに変換するためのライブラリです。
下記コマンドでインストールしたら、ファイルを修正します。
npm install html-react-parser
import Image from "next/image";
import styles from "./page.module.scss";
import { getArticlesDetail } from "@/libs/microcms";
export default async function Page({ params }) {
// URLパラメータのIDを参照して、ブログの詳細を取得
const article = await getArticlesDetail(params.slug);
return (
<main className={styles.main}>
<div className={styles.article}>
<p className={styles.thumbnail}>
<Image
src={article.thumbnail.url}
alt={article.title}
height={article.thumbnail.height}
width={article.thumbnail.width}
priority
/>
</p>
<h1 className={styles.title}>{article.title}</h1>
<p className={styles.category}>{article.category.name}</p>
<div className={styles.body}>{article.body}</div>
</div>
</main>
);
}
取得件数を設定する
microCMSでは、limit
パラメータを使用することで取得件数を指定することができます。
実はデフォルト値ではlimit
の値が10
となっており、10件までしか取得できないので11件以上取得する場合は変更が必要です。
また、上限値は100
となっており、101件以上取得するにはmicroCMS公式のヘルプ(101件以上のコンテンツを取得するにはどうしたらよいですか?)で紹介されていますのでそちらを参照ください。
limit
パラメータはクエリパラメータとして指定できます。
試しに1件のみ取得するように設定します。
import { notFound } from "next/navigation";
import styles from "./page.module.scss";
import { Cards } from "@/components/Cards";
import { getArticlesList } from "@/libs/microcms";
export default async function Home() {
// ブログ一覧を取得
const queries = { limit: 1 };
const articlesListResponse = await getArticlesList(queries).catch(() =>
notFound()
);
const { contents: articles } = articlesListResponse;
return (
<main className={styles.main}>
<h1>記事一覧</h1>
<ul className={styles.cards}>
<Cards articles={articles} />
</ul>
</main>
);
}
1件のみの取得に成功しました。
将来的にページネーション機能を追加し、例えば「1ページにつき12件まで表示」といった設定を行う場合、取得件数の設定を一箇所で管理できると保守性が良いので、limit
の値を管理するためのファイルを作成し、それを読み込み使用します。
管理するファイルはconstants
フォルダを作成し、その中で管理するようにしました。
// 1ページの表示件数
export const LIMIT = 12;
import { notFound } from "next/navigation";
import styles from "./page.module.scss";
import { LIMIT } from "@/constants";
import { Cards } from "@/components/Cards";
import { getArticlesList } from "@/libs/microcms";
export default async function Home() {
// ブログ一覧を取得
const queries = { limit: LIMIT };
const articlesListResponse = await getArticlesList(queries).catch(() =>
notFound()
);
const { contents: articles } = articlesListResponse;
return (
<main className={styles.main}>
<h1>記事一覧</h1>
<ul className={styles.cards}>
<Cards articles={articles} />
</ul>
</main>
);
}
これでmicroCMSからのデータ取得の基本は完了です。
6. SSGに対応する
現段階では、一覧ページはSSG(Static Site Generation)の静的なファイルとして生成してくれていますが、各記事の詳細ページはSSR(Server-Side Rendering)のリクエストごとにサーバーでページをレンダリングする手法になっています。
試しに、現在の状態でnpm run build
を実行すると、コンソールには次のように表示されます。
> next build
▲ Next.js 14.2.3
- Environments: .env.local
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (5/5)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 7.07 kB 99.2 kB
├ ○ /_not-found 871 B 87.8 kB
└ ƒ /articles/[slug] 285 B 92.4 kB
+ First Load JS shared by all 87 kB
├ chunks/23-0627c91053ca9399.js 31.5 kB
├ chunks/fd9d1056-2821b0f0cabcd8bd.js 53.6 kB
└ other shared chunks (total) 1.88 kB
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
ƒ /articles/[slug]
となっており、ƒ
マークはSSRであることを表しています。
ここからは詳細ページもSSGで事前にHTMLの静的ファイルが生成されるようにファイルを編集します。
SSGにするためには、generateStaticParams
を使用します。
generateStaticParams
はNexet.jsのApp Routerで導入された機能で、動的ルートのために静的パスを生成するために使用されます。
この機能は、以前のバージョンのNext.jsで使用されていたgetStaticPaths
メソッドの代わりに利用されます。
import Image from "next/image";
import parse from "html-react-parser";
import styles from "./page.module.scss";
import { getArticlesList, getArticlesDetail } from "@/libs/microcms";
export async function generateStaticParams() {
// ブログ一覧をAPI経由で取得します
// 取得件数には最大値である`100`を設定(初期値:10)
// データ取得量の削減のため、コンテンツ内の'id'のみ取得
const queries = { limit: 100, fields: "id" };
const articlesListResponse = await getArticlesList(queries);
// `articlesListResponse`の`contents`を`articles`に代入しています。
const { contents: articles } = articlesListResponse;
// 各ブログポストのIDを用いて、必要なパラメータオブジェクトを作成
// 'articleId'キーに対応する値として'article.id'を設定
// これにより、各生成されるページに対して、どのブログポストのデータを
// 取得して表示するかを指定するための情報を提供します
const paths = articles.map((article) => {
return {
slug: article.id,
};
});
return [...paths];
}
export default async function Page({ params }) {
// URLパラメータのIDを参照して、ブログの詳細を取得
const article = await getArticlesDetail(params.slug);
return (
<main className={styles.main}>
<div className={styles.article}>
<p className={styles.thumbnail}>
<Image
src={article.thumbnail.url}
alt={article.title}
height={article.thumbnail.height}
width={article.thumbnail.width}
priority
/>
</p>
<h1 className={styles.title}>{article.title}</h1>
<p className={styles.category}>{article.category.name}</p>
<div className={styles.body}>{parse(article.body)}</div>
</div>
</main>
);
}
さきほどの取得件数(limit
)でも説明しましたが、取得件数のデフォルト値は10
件となっているので、最大値である100
を指定しています。
取得する情報はコンテンツID以外不要なので、fields
でid
のみを取得し、データ量の削減をしています。
この状態で、試しにpaths
の中身がどうなっているのかコンソールに表示してみます。
console.log("paths => ", paths);
// 出力結果
paths => [ { slug: '2la1jxzdhmy' }, { slug: 'yy3_bxzq2f-0' } ]
この状態でnum run build
を実行します。
> next build
▲ Next.js 14.2.3
- Environments: .env.local
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (7/7)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 7.07 kB 99.2 kB
├ ○ /_not-found 871 B 87.8 kB
└ ● /articles/[slug] 285 B 92.4 kB
├ /articles/2la1jxzdhmy
└ /articles/yy3_bxzq2f-0
+ First Load JS shared by all 87 kB
├ chunks/23-0627c91053ca9399.js 31.5 kB
├ chunks/fd9d1056-2821b0f0cabcd8bd.js 53.6 kB
└ other shared chunks (total) 1.88 kB
○ (Static) prerendered as static content
● (SSG) prerendered as static HTML (uses getStaticProps)
先ほどまでƒ /articles/[slug]
であったのが、● /articles/[slug]
に変わっており、SSGになっていることが確認できます。
7. Vercelでホスティング
前提条件
- 事前にデプロイを行うGitHubのリポジトリを用意していること
- Vercelにサインアップしていること
Vercelのリポジトリ一覧にプロジェクトを追加
ダッシュボード内の「Add New…」ボタンをクリックし、プルダウンの中から「ProJect」を選択
「Continue With GitHub」をクリックしてVercel1とGit Hubを連携する。
GitHub内の全てのリポジトリをVercelでインポート可能にする場合は「All repositories」、特定のリポジトリのみインポート可能にする場合は「Only select repositories」を選択。
今後Vercelで他にもホスティングしていきたいので「All repositories」を選択しました。
選択したら「Install」をクリック。
Vercelでデプロイする
プロジェクト名を変更する場合は「Project Name」を変更
microCMSと連携するために環境変数の設定を行う。
ここでは、.env.local
ファイルで入力した環境変数と同じ内容を入力する。
「Environment Variables」を開く。
「Key」と「Value (Will Be Encrypted)」項目を入力。
MICROCMS_SERVICE_DOMAIN
とMICROCMS_API_KEY
の2つを追加する。
入力したら「Deploy」をクリック。
デブロイが成功すると「Congratulations!」というテキストとともに紙吹雪が舞う。
デプロイしたプロジェクトがホスティングできているかを確認する。
表示されているページ画像をクリックするとホスティングされたページへ飛ぶ。
API_KEYを後から追加する場合
プロジェクトのダッシュボードから上部メニューの「Settings」を開く
左メニューから「Environment Variables」を開く
「Key」と「Value」を入力して「Save」で完了
8. 記事投稿で自動デプロイ
記事更新時に自動デプロイが行われ、自動でサイト更新がされるように設定します。
VercelでWeb HookのURを発行
Vercelのダッシュボートから該当プロジェクトを開き、上部メニューから「Settings」を開く
左メニューから「Git」を開き、その中の「Deploy Hooks」項目にある「Create Hook」を入力する
左入力エリアの「My Example Hook」には好きな名前を入力。(今回は「main」としました。)
右入力エリアにはブランチ名を入力。(例:main [または master])
入力したら「Create Hook」をクリック。
そうするとWeb HookのURLが発行されるのでコピーする
microCMSでWebhook登録
microCMSの管理画面を開き、左の「コンテンツ(API)」から連携したいAPIを開く
右上の「API設定」を開き、左メニューから「Webhook」を開き、「追加」ボタンをクリック
Webhook通知サービスの選択が表示される、ここで一番下の「カスタム通知」を選択
下記の通り設定しました。
Webhookの名前:好きな名前を入力(例:blog)
URL:Vercelで発行したWeb Hook URL
「通知タイミングの設定」はどのタイミングでデプロイするかの設定になるはずです。
今回は初期設定のままにしておきます。
最後に「設定する」をクリックして完了です。
確認
microCMSで記事を投稿するとVercelで自動デプロイされてサイト更新がされるか確認します。
microCMSで適当な記事を作成し公開します。
その後、Vercelで確認します。
自動的にbuildが実行され、そのまま問題なければ完了します。
サイトを確認すると、表示が更新されているのを確認できました。
さいごに
Next.js × microCMS の基本をまとめました。
他にもカテゴリページの作成、メタデータ生成、サイトマップ生成などやれることはありますが、書くとキリがないので今回はここまでのご紹介とさせていただきます。
microCMS公式では、Next.jsとの連携方法についてチューリアルが用意されていますので、そちらを参考にすると体得しやすいと思います。
また、microCMSがブログテンプレートのGitHubも公開されているので、そちらもぜひ参考にしてみてください。
参考
【完全版】App Routerで最初に知っておくとよさそうな基礎を全部まとめてみた
【初心者OK】Next.jsで自作ブログを作ってみよう【MicroCMSを利用】
チュートリアル(App Router)|microCMSドキュメント
microCMS + Next.jsでJamstackブログを作ってみよう
Vercelの新規登録からNext.jsデプロイまで
Vercelで環境変数を設定する