star back image
people4
電飾 電飾
moon
astronaut

microCMS × Next.js で固定ページ化をしたい【Cursor】

BLOG AIWEBログ
読了約:44分

microCMSとNext.jsの組み合わせは、Web業界のフロントエンド制作において人気があり、案件でも重宝される構成です。

へぇ、人気なんですか?

固定ページ化ですから、DBからの読み込みがないので表示するスピードが早いです。固定ゆえセキュリティーも高い。

大枠の仕組みの流れ
[ microCMS(管理画面) ]
  ↓ (APIでテキストや画像データを送信)
[ Next.js(ビルド処理) ] 
  ↓ (データを流し込んで静的HTMLを作成)
[ 超高速な固定ページWebサイト ]
どうするの?固定ページ化をまったく想像できなかった

Cursorに作成してもらったファイルをアップしました。

403 Errorっなんで?AIに質問します。

なぜFTPでアップしたのに403エラーになるのか?

これまでのPHP(index.php など)は、「注文を受けてから料理を作るレストラン」です。 サーバーに .php ファイルを置いておけば、お客さんがアクセスした瞬間にサーバーがmicroCMSにデータを取りに行き、HTMLを組み立てて表示してくれます。

しかし、Next.jsのファイル(.js)はそのままサーバーに置いても動きません。 Next.jsにおけるSSG(静的サイト生成)は、「事前にお弁当を作ってショーケースに並べておくお弁当屋さん」です。

つまり、あなたのMac(パソコン)の中で事前にHTMLファイルを完成させてから、その完成品(お弁当)だけをFTPでアップロードする必要があります。

パソコンの中でやるんですと。

つまり固定ページ化は「ローカルでやる」ということですね。

そういうことです。
そのためにはまずNode.jsがいるというのです。

Node.jsってなに?ほえー

Cursor(AI)を使って一気に作るためのプロンプト作成

microCMSは以前にテストしたことがあり、APIの読み込みを体験しています。

microCMの体験記事
https://neo.astrowave.jp/blog/9810/

APIを読み込みテスト
https://astrowave.jp/microcms/

以下のような感じです。

index.php (Blogトップ)

<!-- content -->
<section class="main">
  <div id="result1"></div>
</section>

<!--Categories-->
<section class="categories">
  <div id="result1_2"></div>
</section>

<!--記事内容-->
<section class="content">
  <div id="result1_3"></div>
</section>


<script>
window.onload = function() {

  const API_KEY = '●●●●●●●●●●●●●●●●●●●●●●●●●●●'; // 必要に応じてAPIキーを設定
  const API_URL_BASE = 'https://astrowave.microcms.io/api/v1';

  let apiUrl;

  apiUrl = 'https://astrowave.microcms.io/api/v1/banner/?limit=100';
  headers = { 'Content-Type': 'application/json', 'X-MICROCMS-API-KEY': API_KEY };

  // APIエンドポイントとパラメータを確認
  console.log(`Fetching from URL: ${apiUrl}`);


  // Fetch requests
  fetch(apiUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-MICROCMS-API-KEY': API_KEY } })
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return response.json();
    })
    .then(data => {
      renderData1(data);
      renderData1_2(data);
      renderData1_3(data);
    })
    .catch(error => console.error('Fetching error:', error));


    function renderData1(data) {
      const result1 = document.getElementById('result1');

      // 画像を含む他の要素のレンダリング方法を定義するテンプレート関数
      const html = `
        <div class="mv-box">
          <div class="image">
            ${data.image ? `<img src="${data.image.url}" alt="Image">` : ''}
          </div>
          <div class="info-link">
            <a href="${data['url']}" class="" target="_blank">${data['url']}
              <svg class="up-arrow" xmlns="http://www.w3.org/2000/svg" width="11.318" height="11.046" viewBox="0 0 11.318 11.046"><g transform="translate(-113.655 0.652)">
              <path d="M6,0V6H0" transform="translate(118.273 6.048) rotate(-90)" fill="none" stroke="#111" stroke-width="1.4"/>
              <path d="M14,0H0" transform="translate(114.15 9.899) rotate(-45)" fill="none" stroke="#111" stroke-width="1.4"/></g>
              </svg>
            </a>
          </div>
          <div class="info-box">
            <p class="top-txt">${data['description']}</p>
          </div>
        </div>
      `;

      // HTMLをDOMに挿入
      result1.innerHTML = html;

    }

    // Categoriesの内容
    function renderData1_2(data) {
      fetch('https://astrowave.microcms.io/api/v1/categories?limit=100', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'X-MICROCMS-API-KEY': API_KEY
        },
      })
      .then(response => response.json())
      .then(data => {
        const result1_2 = document.getElementById('result1_2');
        const contents = data.contents;
        let html = '';

        contents.forEach(item => {
          html += `
            <div class="list">
              <p class="stores-list-ttl">${item['name']}</p>
            </div>
          `;
        });

        result1_2.innerHTML = html;
      })
      .catch(error => {
        console.error('Error fetching data:', error);
      });
    }

    // 記事内容
    function renderData1_3(data) {
      fetch('https://astrowave.microcms.io/api/v1/blogs?limit=100', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'X-MICROCMS-API-KEY': API_KEY
        },
      })
      .then(response => response.json())
      .then(data => {
        const result1_3 = document.getElementById('result1_3');
        const contents = data.contents;
        let html = '';
        console.log(data);

        contents.forEach(item => {
          // updatedAtの日時を取得し、フォーマットを変更
          const updatedAt = new Date(item.updatedAt);
          const formattedDate = `${updatedAt.getFullYear()}/${String(updatedAt.getMonth() + 1).padStart(2, '0')}/${String(updatedAt.getDate()).padStart(2, '0')}`;

          html += `
            <div class="list">
              <a href="/microcms/blog-detail.php?id=${item.id}" class="blog-title">
                <div class="list-eyecatch">${item.eyecatch ? `<img src="${item.eyecatch.url}" alt="Image">` : ''}</div>
                <p class="list-category"><span class="catego">${item['category'].name}</span> ${formattedDate}</p>
                <p class="list-ttl">${item['title']}</p>
              </a>
            </div>
          `;
        });

        result1_3.innerHTML = html;
      })
      .catch(error => {
        console.error('Error fetching data:', error);
      });
    }

};
</script>

detail.php(記事詳細ページ)

<!-- content -->
<section class="blog-detail">
    <div id="result1_3"></div>
    <div id="pagination">
      <a id="prevPost" href="#" style="display: none;">PREV</a>
      <a id="nextPost" href="#" style="display: none;">NEXT</a>
  </div>
</section>


<script>
window.onload = function() {
  const API_KEY = '●●●●●●●●●●●●●●●●●●●●●●●●●●●'; // 必要に応じてAPIキーを設定
  const API_URL_BASE = 'https://astrowave.microcms.io/api/v1';

  const urlParams = new URLSearchParams(window.location.search);
  const postId = urlParams.get('id');

  // 全てのブログ投稿を取得
  fetch(`${API_URL_BASE}/blogs?limit=100`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json', 'X-MICROCMS-API-KEY': API_KEY }
  })
  .then(response => response.json())
  .then(data => {
    const posts = data.contents;
    const currentIndex = posts.findIndex(post => post.id === postId);

    // 現在の投稿データを表示
    const post = posts[currentIndex];
    renderPostDetail(post);

    // 前後の投稿を設定
    if (currentIndex > 0) {
      const prevPost = posts[currentIndex - 1];
      document.getElementById('prevPost').href = `/microcms/blog-detail.php?id=${prevPost.id}`;
      document.getElementById('prevPost').style.display = 'block';
    }

    if (currentIndex < posts.length - 1) {
      const nextPost = posts[currentIndex + 1];
      document.getElementById('nextPost').href = `/microcms/blog-detail.php?id=${nextPost.id}`;
      document.getElementById('nextPost').style.display = 'block';
    }
  })
  .catch(error => console.error('Error fetching data:', error));

  // 現在の投稿詳細を表示する関数
  function renderPostDetail(post) {
    const postDetail = document.getElementById('result1_3');
    postDetail.innerHTML = `
      <div class="image">
        ${post.eyecatch ? `<img src="${post.eyecatch.url}" alt="Image">` : ''}
      </div>
      <h1>${post.title}</h1>
      <p>${new Date(post.updatedAt).toLocaleDateString()}</p>
      <div>${post.content}</div>
    `;
  }
}
</script>

上記のAPIの読み込みを体験をAIに見てもらいました。

JavaScript(JavaScriptの fetch でブラウザから直接microCMSのAPIを叩く方式)は、仕組みがとても直感的で素晴らしいですね。すでにバナー、カテゴリー一覧、ブログ一覧、さらには詳細ページでの前後記事(PREV/NEXT)のID判定までしっかりと実装されています。

今までのJavaScript方式(CSR: クライアントサイドレンダリング)と、これから作成する micro_next(Next.jsを使ったSSG:静的サイト生成) の最大の違いは、以下の点です。
今まで(PHP + JavaScript)とこれから(Next.js)の違い
比較項目今までのコード (/microcms/)これからのNext.js (/micro_next/)
画面表示の流れ画面が開いた後、ブラウザがAPIを叩いてデータを読み込む(一瞬白画面や読み込み待ちが発生)ビルド時(またはサーバー側)にあらかじめHTMLを作ってしまうため、開いた瞬間に画面が表示される(超高速!)
SEO(検索エンジン)GoogleなどがJavaScriptを実行して評価するのを待つ必要がある最初から完璧なHTMLが存在するため、SEOに圧倒的に強い
APIキーの安全性ブラウザのソースコードに API_KEY が丸見えになっているサーバー側で処理するため、APIキーを完全に隠せる

なるほど。。何を言っているのか。わかります?

こんなことも解らないのですか。情けない。

ディレクトリの作成とNext.jsのセットアップ

まず大前提として、自分のパソコン(Mac/Windows)に『Node.js』がインストールされている必要があります。これがないとNext.jsは動きません。
公式サイトからLTS版をダウンロードしてインストールしましょう。

以下をターミナルで実行します。

# プロジェクト(micro_next)の作成
npx create-next-app@latest micro_next

# 作成したフォルダに移動
cd micro_next

# microCMS公式ライブラリをインストール
npm install microcms-js-sdk
Cursor(AI)を使って、既存コードをNext.js化する指示文

各記事の個別ページを固定ページとして生成するため、generateStaticParams を使ってあらかじめ存在する記事のID一覧(パス)をNext.jsに伝えます。

↑上のAPIの読み込みを体験を見せた感じです。

既存のJavaScriptコードの設計(バナー、カテゴリー、ブログ一覧、詳細ページのPREV/NEXTリンク)をそのままNext.jsに移植してもらうためのプロンプトを作成しました。

Cursorで micro_next フォルダを開き、以下の指示文を貼り付けてみてください。

※CursorではAIがチャット画面でコードを出力してくれるので、Cursorの機能(Applyボタンなど)を使って、それぞれのファイルにコードを適用(またはコピペ)していきます。

プロンプトは下のこれです↓

Cursorに頼むために作成したプロンプト

microCMSのデータを使用して、Next.js(App Router)でSSG(静的サイト生成)による超高速なブログ・ポータルサイトを作成したいです。

既存のHTML/JavaScriptコードで利用しているmicroCMSの設定とAPI構造を以下に記載します。
これをベースに、Next.jsの推奨設計でコードを作成してください。

【接続情報】
・serviceDomain: "astrowave"
・APIキー: 環境変数(.env.local)で管理するようにしてください(変数名: MICROCMS_API_KEY)

【取得したいエンドポイントと仕様】
1. バナー(/banner): アイキャッチ画像、説明文、リンクURLを表示
2. カテゴリー(/categories): カテゴリー名一覧を表示
3. ブログ一覧(/blogs): 記事一覧(アイキャッチ、カテゴリー名、更新日「YYYY/MM/DD」、タイトル、詳細へのリンク `/blogs/[id]`)
4. ブログ詳細(/blogs/[id]):
   - generateStaticParams を使用して各記事をビルド時に固定ページ化(SSG)
   - アイキャッチ、タイトル、更新日、本文(HTML描画)
   - 前後の記事(PREV/NEXT)へのリンク処理

【作成してほしいファイル構造】
1. `.env.local` (環境変数用サンプル)
2. `libs/client.js` (microCMS SDKの初期化)
3. `app/page.js` (バナー、カテゴリー、ブログ一覧をまとめたトップページ)
4. `app/blogs/[id]/page.js` (ブログ詳細ページ+PREV/NEXT判定)

コードには、初心者でも追いやすいように解説コメントを適宜記載してください。

(※ここに、「既存のHTMLとJSのコード(fetchしている部分など)」をそのまま貼り付けます)

最後の行:
(※ここに、「既存のHTMLとJSのコード(fetchしている部分など)」をそのまま貼り付けます)は、前に貼った「APIの読み込みを体験」のコード(index.php とdetail.php)をまるっと貼り付け追記しています。

実は最初、AIに「要件」だけを伝えてコードを作ってもらいました。
すると、機能は動くのですが、HTMLのタグやクラス名が一般的なものになってしまい、自分のサイトのCSS(デザイン)が全く効かない状態になってしまいました。

既存のコード(HTML/JS)も一緒にプロンプトに貼り付けて、『これを翻訳して』と頼む。
AIを使う時は「要件」だけでなく「今の状態(既存コード)」もセットで渡すのが、スマートに開発を進める最大のコツかなと。

プロンプトによってCursorの作成してくれたファイルが、micro_nextフォルダに出現します。

いよいよ動作させよう「ビルド手順」

Next.jsを動かすための「設計図」などはプロンプトのおかげで全てあります。いよいよ静的HTML(お弁当)を作ります!

ビルドする時は必ずそのフォルダの中にいる必要がある

ターミナルアレルギーのある人もいると思いますが、もうすぐ終わります。

1.フォルダに移動する

cd micro_next

もしターミナルを開き直した場合は、作成した micro_next フォルダに移動(cd)してください。

2.ビルドを実行する

npm run build

run buildをしたら、outというフォルダが出現したと思います。
そこに固定ページ化されたhtmlが入っています。

FTPでアップロードして、URLを確認しましょう。

固定ページ化(microCMS)
https://astrowave.jp/micro_next/

改めてブログに書くべき「正しい手順」

1.Node.jsのインストール(超重要・事前準備)

まず大前提として、自分のパソコン(Mac/Windows)に『Node.js』がインストールされている必要があります。これがないとNext.jsは動きません。
公式サイトからLTS版をダウンロードしてインストールしましょう。

2.プロジェクトの作成とセットアップ

ターミナル(黒い画面)を開いて、以下のコマンドを順番に打ち込んでいきます。

# ① Next.jsのプロジェクト(micro_next)を作成する
npx create-next-app@latest micro_next

# ② 作成したフォルダの中に移動する
cd micro_next

# ③ microCMSと通信するための公式ライブラリをインストールする
npm install microcms-js-sdk
3. AIにプロンプトを投げてコードを作ってもらう【Cursorの出番!】

Cursorにプロンプトを使って、以下のファイルを作成してもらいます。

micro_next/
├── .env.local ←【新規】APIキーなどの環境変数を書く
├── next.config.mjs ←【編集】ロリポップ用の魔法の設定を書く
├── libs/
│ └── client.js ←【新規】microCMSと通信するための設定
└── app/
├── globals.css ←【編集】サイト全体のCSSをまとめる
├── layout.js ←【編集】全ページ共通の枠組み(CSSの読み込みなど)
├── page.js ←【編集】トップページ(バナー、カテゴリ、記事一覧)
└── blogs/
└── [id]/
└── page.js ←【新規】ブログ詳細ページ(SSGで自動生成される)

※CursorがAIがチャット画面でコードを出力してくれます。
4.魔法の設定(ロリポップ対策)

コードが書けたら、next.config.mjs に以下の設定を追加します。これをしないと、ロリポップにアップした時に下層ページが404エラーになります。

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  basePath: '/micro_next',
  images: {
    unoptimized: true,
  },
  // ③ リンクの末尾に .html を付ける設定を追加
  trailingSlash: true,
};

export default nextConfig;

※【補足】を参照

5.ビルド(静的HTMLの生成)

準備ができたら、ターミナルで以下のコマンドを実行します。

npm run build

成功すると out というフォルダが作られます。この中身が完成したHTMLファイルHTMLの束です。

6. FTPでアップロード

最後に、FTPソフトを使って out フォルダの中身すべて(_next フォルダも忘れずに!)をサーバーにアップロードすれば完成です。

固定ページ化(microCMS)
https://astrowave.jp/micro_next/

【補足】ロリポップなどの一般的なレンタルサーバーの仕様

手順の4の補足になります。

ロリポップ等、魔法の設定とありますが。なんですか?

ブログ詳細ページが404になる原因は、「ロリポップなどの一般的なレンタルサーバーの仕様」にあります。

Next.jsは 7u3b6lplvld.html というファイルを作っていました。 しかし、ブラウザのURLは https://astrowave.jp/micro_next/blogs/7u3b6lplvld となっており、末尾に .html が付いていません。 Next.js専用のサーバー(Vercelなど)なら自動で .html を補って表示してくれるのですが、ロリポップのサーバーは「.htmlがないとファイルを見つけられない!」となって404エラーを出してしまいます。

解決策:Next.jsの設定を変更して再ビルドしました!
ロリポップのようなサーバーでも綺麗に動くように、Next.jsの設定(next.config.mjs)に trailingSlash: true という魔法の1行を追加し、私の方で再度ビルドを実行しました。

これにより、out/blogs/7u3b6lplvld.html だったファイルが、 out/blogs/7u3b6lplvld/index.html という「フォルダ+index.html」の構造に変換されました。 これなら、ロリポップのサーバーでも .../7u3b6lplvld/ にアクセスした瞬間に自動で index.html を読み込んでくれます!

どうやらそのままビルドすると、ロリポップなどのレンタルサーバーでは下層ページが404エラーになるようです。。

これを防ぐために next.config.mjs に以下の設定をいれてくれたという。

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  basePath: '/micro_next', // 自分の環境に合わせて変更
  images: { unoptimized: true },
  trailingSlash: true, // ← これが404を防ぐ魔法の設定!
};
export default nextConfig;
絶対に必要なものなの?

当然解らず、AIに聞きます。

使うサーバーの種類によって、この「魔法の設定(trailingSlash: true)」が必要かどうかが明確に分かれます。

「Vercel(ヴァーセル)」や「Netlify(ネットリファイ)」といった、Next.jsを動かすために作られた最新の専用サーバーを使う場合、この設定は不要です。

一方、ロリポップ、エックスサーバー、さくらサーバーなどの、昔からある伝統的な「Apache(アパッチ)」や「Nginx(エンジンエックス)」という仕組みで動いているサーバーを使う場合は、この設定が絶対に必要になります。

そうなんですね。魔法とか言うのでアレです。

外部のJSライブラリ(jQueryやスライダーなど)を使いたい場合は?

「CSSと同じように外部ファイル(.js)を作って読み込ませる」というやり方は、Next.js(React)では基本的にはしないそうです。

これまでのWeb制作では、<script src="script.js"></script> のように書いて、その中で document.getElementById('btn').addEventListener(...) のように「HTMLの要素を探して動きをつける」のが普通でした。

しかし、Next.js(React)は「HTMLとJSを一体化させて書く(JSX)」というルールで動いています。 そのため、外部から無理やりJSでHTMLを操作しようとすると、Next.jsの仕組みと喧嘩してしまい、バグの原因になります。

例えば、トップページ(app/page.js)に「クリックしたらアラートが出るボタン」を追加したい場合は、以下のように書きます。
// ① 一番上に「これはブラウザで動くJS(クライアントコンポーネント)ですよ」という宣言を書く
'use client';

import Link from 'next/link';
import { client } from '../libs/client';

export default function Home() {
  // ② ここにJSの関数(動き)を書く!
  const handleButtonClick = () => {
    alert('ボタンがクリックされました!');
  };

  return (
    <main>
      {/* ③ HTMLタグの中に、直接 onClick などのイベントを書く! */}
      <button 
        onClick={handleButtonClick} 
        style={{ padding: '10px', background: '#333', color: '#fff' }}
      >
        ここをクリック
      </button>

      {/* 以下、バナーやブログ一覧の表示... */}
    </main>
  );
}

どうしても外部のJSライブラリ(jQueryやスライダーなど)を使いたい場合は?

「自分で書くJS」ではなく、「世の中にある便利なJSライブラリ」を使いたい場合はどうするか。 これも <script> タグで読み込むのではなく、ターミナルで npm install を使って部品としてダウンロードし、ファイルの中で import して使うのがモダンなやり方です。

(例:Swiperというスライダーを使いたい場合)

1.ターミナルで npm install swiper を実行
2.page.js の一番上で import { Swiper, SwiperSlide } from 'swiper/react'; と書いて読み込む
3.HTMLの中で <Swiper> タグを使う

ちょっと面倒な気がします。

運用していく上で発生するシチュエーション

例えば、microCMSのブログ記事に「著者名(author)」というテキストフィールドを新しく追加したとします。

Next.js(React)の場合、microCMSで項目が増えた時の対応は「めちゃくちゃ簡単」です。 なぜなら、APIから取得したデータ(JSON)は、すでに丸ごと変数(post など)の中に入っているからです。

簡単らしいです。

やるべきことは「たったの3ステップ」

  1. コードに {post.フィールドID} を書き足す
  2. npm run build でビルドし直す
  3. FTPでアップロードする
ステップ1:コード(page.js)に表示したい場所を書き足す

app/blogs/[id]/page.js のHTML(JSX)部分を開いて、表示させたい場所に {post.author} と書き足すだけです。

【修正前】
<h1>{post.title}</h1>
<p>{new Date(post.updatedAt).toLocaleDateString()}</p>
{/* dangerouslySetInnerHTMLを使用してHTMLを描画 */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />

↓

【修正後】
<h1>{post.title}</h1>
{/* ↓ 新しく追加した項目! */}
<p className="author-name">著者:{post.author}</p>
          
<p>{new Date(post.updatedAt).toLocaleDateString()}</p>
{/* dangerouslySetInnerHTMLを使用してHTMLを描画 */}
<div dangerouslySetInnerHTML={{ __html: post.content }} />

これだけで、コードの修正は完了です。 PHPの時のように、fetch の処理を書き直したり、新しい変数を定義し直したりする必要はありません。post の中に、microCMSで追加した項目が自動的に全部入ってきています。

ステップ2:もう一度ビルドしてお弁当を作り直す

コードを書き換えたら、ターミナルを開いておなじみのコマンドを打ちます。

cd micro_next
npm run build

これで、新しく「著者名」が入った状態のHTMLファイル(お弁当)が out フォルダの中に一斉に作り直されます。 あとは、それをFTPでサーバーに上書きアップロードするだけです

ステップ3:FTPでアップロードする

FTPでアップしたらページを確認しましょう。以上です。

【AI】イラストを描いてもらった

Flow」で作成した画像です。誰でもgoogleアカウントでログインして使えます。

すごいスピードだニャー!

星間旅路のメロディ

「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」

「この電波はどこの星からきたのだろうか。」

どこかで聞いたことがあるような。