3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

image.png

microCMSを試そう

2024のQiitaアドベントカレンダー一覧をみると、microCMSテーマのカレンダーがあったので記事を書いてみることにしました。
これまで利用したことがありませんが、このような機会に少し触れてみるのも良いですね。

しかし、いざ使ってみようと考えても、目的がないとサービス・道具は意味を持ちません。
今回は架空のキッチンカービジネスのPRサイトをテーマに実装を考えてみたいと思います。1

想定する背景

  • 提携するパン屋のパンを載せて移動販売を行う
  • 車内で簡単な調理を行うメニューもあり
  • 情報発信のためWebサイトを持ちたい

ビジネス的な妥当性や背景はこの記事の趣旨ではないので、あまり深堀りしないものとします。

Webサイトの機能要件

  • カレンダー表示
    • 行き先や定休日などを表示
  • 取り扱いメニュー
    • 期間限定メニューなど更新が簡易なこと
  • お知らせ
    • 簡易に案内を表示できる仕組み
  • ブログ
    • イベントへの出店など、自由に記事を追加できる

技術要件

  • microCMSはひとまずHobby(無料版)で始めたい
  • SSG(Static Site Generation)でS3などにデプロイして運用したい
    • 負荷や不具合の心配を減らす、低コストで運用するため

ワイヤーフレーム

前述の「Webサイトの要件」を満たすようにざっと配置してみるとトップページのワイヤーフレームは下記のような感じでしょうか。

image.png

コンテンツリスト

ページ名 パス
トップページ /
お知らせ一覧 /news/
お知らせ詳細 /news/[id]/
メニュー一覧 /menu/
ブログ記事一覧 /blog/
ブログ記事詳細 /blog/[id]/

v0によるプロトタイプ作成

最初の第一歩はv02にお願いしてみましょう。

スクリーンショット 2024-11-06 11.49.34.png

少し荒いですが、上記リンク先のようなサイトの土台が出来ました。
細かな余白調整などはv0と会話するより、コードを直接編集したりCursorGitHub Copilotと対話したほうが早いので手元にコードを取り込みましょう。

手元で微調整し、ChatGPTのDALL-Eで生成した画像をはめ込んだものが下記の画面です。

image.png

本来はもう少しデザインなどを適用していきたいところですが、本記事はmicroCMSの検証目的のため、ワイヤーフレームっぽい状態のまま進めたいと思います。

今回はSSG想定なので、 next.config.mjs は下記のようにしています。

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
}

export default nextConfig;

これで、 npm run build とすると out ディレクトリ配下に静的なWebサイトのファイル一式が出力されます。

microCMSの適用範囲

営業カレンダーはGoogle Calendarの埋め込みをすれば良いかなと思います。

今回は、microCMSに下記の部分を担ってもらいましょう。

  • お知らせ
  • メニュー
  • ブログ

例えば、メニューカルーセル部分はこの時点では下記のように menus という定数にハードコーディングしていますが、これがAPIから取得できればよいわけですね。

MenuCarousel.tsx
export function MenuCarousel() {
  const menus = [
    {
      title: "焼きたて焼きそばパン",
      price: 600,
      isNew: true,
      image: "/images/menu1.webp",
    },
    {
      title: "栗もりもりパン",
      price: 800,
      isNew: true,
      isLimited: true,
      image: "/images/menu-maron.webp",
    },
    // 省略..
  ]
  return (
    <Carousel className="mt-6">
      <CarouselContent>
        {menus.map((menu, index) => (
        <CarouselItem key={index} className="md:basis-1/2 lg:basis-1/4 pt-4">
          <Card className="rounded-none">
            <CardContent className="p-0">
              <div className="relative">
                <img
                  src={menu.image}
                  alt={menu.title}
                  className="aspect-square object-cover"
                  width={300}
                  height={300}
                />
                {menu.isLimited && (
                  <span className="absolute left-2 top-2 rounded bg-primary px-2 py-1 text-xs text-primary-foreground">
                    期間限定
                  </span>
                )}
                {menu.isNew && (
                  <span className="absolute right-0 top-0 w-10 h-10 translate-x-1/3 -translate-y-1/3 flex items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
                    NEW
                  </span>
                )}
              </div>
              <div className="p-4">
                <h3 className="font-bold">{menu.title}</h3>
                <p className="mt-1 text-lg font-bold">¥{menu.price}</p>
              </div>
            </CardContent>
          </Card>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
} 

microCMSのアカウント作成

https://microcms.io/ へアクセスして、「無料ではじめる」から登録します。
クレジットカードの登録不要というのは地味に嬉しいですね。試してみるハードルがグッと下がります。

image.png

特に操作に迷うことなく、すぐにアカウント登録が出来ました。
アンケート回答後にサービス作成画面へ遷移します。

microCMSでのサービス作成手順

「テンプレートから選ぶ」機能もあるようですが、今回は入出力したいデータが明確なので、「一から作成する」を選んでみます。

image.png

後から変更可能であるため、詳細な設定は後ほど行うこととし、まずは登録を進めます。

image.png

API作成

APIを作成します。

image.png

何やら、「ブログ」「お知らせ」と今回の要件にピッタリなものもありますね。

「お知らせ」から作ってみましょう。
ワンクリックで何やら作成できたみたいです。すごい。

何件か試しにお知らせのデータも登録してみます。

image.png

さて、登録したデータはどのようにNext.jsに取り込むと良いでしょうか。
どうやら下記のSDKを用いるみたいです。

オフィシャルチュートリアルも充実しています。

詳細は上記のサイトを確認すると間違いないと思いますが、ここでは要点だけピックアップします。

SDKのインストール

npm install microcms-js-sdk

libs/client.js の作成

import { createClient } from 'microcms-js-sdk';

export const client = createClient({
  serviceDomain: 'service-domain', 
  apiKey: 'api-key',
});

service-domainapi-Key はサービスごとに異なる値のため、該当のサービスに応じた設定が必要です。
service-domain には、作成したサービス「https://XXXX.microcms.io」のXXXX 部分を設定します。api-keyには、自動で作成されたAPIキーの文字列をコピーして入力します。サイドバー下部の「1個のAPIキー」を選択して、APIキー一覧に移動し、作成済みのAPIキーをコピーして貼り付けましょう。

私は前述のとおり「サービス作成」でサービスIDを kitchen-car-pr と入力したので、 service-domainkitchen-car-pr に置き換えます。

api-key は microCMS管理画面のAPIキー管理画面からコピー用ボタンをクリックしてコピーして、api-Keyの部分に貼り付けましょう。

image.png

事前準備は終わりましたので、次に実際にAPIデータを取得し、Next.jsのアプリケーション内で使用しましょう。

newsのエンドポイントからリストを取得するように
libs/client.js に下記を追加します。

libs/client.js
export type News = {
  id: string
  createdAt: string
  updatedAt: string
  publishedAt: string
  revisedAt: string
  title: string
  date: string
}

export type NewsResponse = {
  contents: News[]
  totalCount: number
  offset: number
  limit: number
}

export const getNewsList = async () => {
  const response = await cmsClient.get<NewsResponse>({
    endpoint: 'news',
    queries: { limit: 3 }
  })
  return response.contents
}

consoleで見てみましょう。

image.png

うまく取得出来ていそうです。

多くのWeb APIが日時の曖昧さなくし、マシンリーダブルにするためISO 8601形式などを用いますが、microCMSも同様のようです。
そのようになっていないAPIも世の中にはありますが、困ったことになるので、しっかり標準規格で返却してくれるのはありがたいですね。

とはいっても、 2024-11-06T12:09:38.357Z のような記載のままだと一般ユーザにとってはわかりにくいですね。表示を変換してあげましょう。

npm install date-fns
NewsRecords
import Link from "next/link"
import { format, parseISO } from "date-fns"
import { ja } from "date-fns/locale"

type NewsRecordProps = {
  publishedAt: string
  title: string
  href: string
}

type News = {
  id: string
  publishedAt: string
  title: string
}

function NewsRecord({ publishedAt, title, href }: NewsRecordProps) {
    const formattedDate = format(parseISO(publishedAt), 'yyyy/MM/dd', { locale: ja })
    
    return (
      <div className="flex items-start gap-4">
        <span className="text-sm text-muted-foreground">{formattedDate}</span>
        <Link className="hover:underline" href={href}>
          {title}
        </Link>
      </div>
    )
  } 

export function NewsRecords({ news }: { news: News[] }) {
  return (
    <div className="mt-6 space-y-4">
      {news.map((item) => (
        <NewsRecord key={item.id} publishedAt={item.publishedAt} title={item.title} href="#" />
      ))}
    </div>
  )
} 

これで、publishedAt2024-11-06T12:09:38.357Z だった場合、
formattedDate2024/11/06 のようになります。

image.png

日付書式はうまくいきましたが、APIからの返却が publishedAt の降順になっていないので、 並びに違和感があります。
(デフォルトでは createdAt の降順のようですが、日付を任意のものを指定したいことがあるため、ここでは publishedAt を用います)

getNewsList にソート用のパラメータを追加しましょう。

client.ts
export const getNewsList = async () => {
  const response = await client.get<NewsResponse>({
    endpoint: 'news',
    queries: { limit: 3, orders: '-publishedAt' }
  })
  return response.contents
}

image.png

これで想定通りの並びになりました。

お知らせ詳細ページの実装

コンテンツリストで記載したとおり、/news/[id]/ のようなパスで詳細ページを作成します。
まずは client.ts を修正してNewsのIDリストを返却する関数を追加しましょう。

client.ts
export const getNewsIdList = async () => {
  const ids = await client.getAllContentIds({
    endpoint: 'news'
  })
  return ids
}

app/news/[id]/page.tsx を作成して、下記のようにします。

app/news/[id]/page.tsx
import { getNewsDetail, getNewsIdList } from "@/lib/client"
import { format, parseISO } from "date-fns"
import { ja } from "date-fns/locale"

type Props = {
  params: {
    id: string
  }
}

export async function generateStaticParams() {
  const newsIds = await getNewsIdList()
  return newsIds.map((id) => ({
    id: id.toString(),
  }))
}

export default async function NewsDetail({ params }: Props) {
  const news = await getNewsDetail(params.id)
  const formattedDate = format(parseISO(news.publishedAt), 'yyyy/MM/dd', { locale: ja })
  
  return (
    <div className="max-w-4xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">{news.title}</h1>
      <div className="text-gray-600 mb-4 text-right">{formattedDate}</div>
      { news.content && (
        <div 
          className="prose"
          dangerouslySetInnerHTML={{ __html: news.content }}
        />
      )}
    </div>
  )
} 

generateStaticParams によって生成すべきURLのリストを解決しています。

これにより、ビルド時に /news/sywwycx7f.html のようなIDがファイル名となるHTMLファイルが生成されるようになりました。

メニューのAPI実装

ブログ機能はお知らせ機能と同様にテンプレートを選択することによって進められそうなので、次にメニューのAPIを考えます。

管理画面の左メニュー「コンテンツ(API)」の「+」マークからAPIを作成します。

image.png

次は「自分で決める」を選択してみましょう。

image.png

リスト形式を選択。

image.png

スキーマはこんな感じに。

image.png

作成すると「コンテンツがありません」となるので「追加」ボタンから追加してみましょう。

image.png

入力している中で、期間限定のフラグを入れ忘れていたことに気が付きました。
下記のように問題なくフィード追加できました。

image.png

「変更する」ボタンを押下すると下記のようなダイアログが出てきます。

image.png

本来、スキーマ変更は危険で破壊的な操作になる恐れがありますので、とても配慮頂いた実装ですね。

改めてコンテンツを入れて行きます。
入れ終わるとこんな感じ。

image.png

APIプレビューを確認するとこんな感じ。

{
    "contents": [
        {
            "id": "4-fk-b0dm",
            "createdAt": "2024-11-18T07:46:28.580Z",
            "updatedAt": "2024-11-18T07:46:28.580Z",
            "publishedAt": "2024-11-18T07:46:28.580Z",
            "revisedAt": "2024-11-18T07:46:28.580Z",
            "title": "甘口カレーパン",
            "price": 600,
            "isNew": false,
            "isLimited": false,
            "image": {
                "url": "https://images.microcms-assets.io/assets/6acf32a5bf0543a8b88d18e6fabd693c/f3cc1c834a604f678a1f95ebefff090b/menu-curry-sweet.webp",
                "height": 1024,
                "width": 1024
            }
        },
        ... 省略 ..
    ]
}

該当部分をAPIからデータ取得するように変更しましょう。

MenuCarousel.tsx
export async function MenuCarousel() {
  const menus = await getMenuList()

  return (
    <Carousel className="mt-6">
      <CarouselContent>
        {menus.map((menu) => (
        <CarouselItem key={menu.id} className="md:basis-1/2 lg:basis-1/4 pt-4">
          <Card className="rounded-none">
            <CardContent className="p-0">
              <div className="relative">
                <img
                  src={menu.image.url}
                  alt={menu.title}
                  className="aspect-square object-cover"
                  width={300}
                  height={300}
                />
                {menu.isLimited && (
                  <span className="absolute left-2 top-2 rounded bg-primary px-2 py-1 text-xs text-primary-foreground">
                    期間限定
                  </span>
                )}
                {menu.isNew && (
                  <span className="absolute right-0 top-0 w-10 h-10 translate-x-1/3 -translate-y-1/3 flex items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
                    NEW
                  </span>
                )}
              </div>
              <div className="p-4">
                <h3 className="font-bold">{menu.title}</h3>
                <p className="mt-1 text-lg font-bold">¥{menu.price}</p>
              </div>
            </CardContent>
          </Card>
          </CarouselItem>
        ))}
      </CarouselContent>
      <CarouselPrevious />
      <CarouselNext />
    </Carousel>
  )
} 
client.ts
export const getMenuList = async () => {
  const response = await client.get<MenuResponse>({
    endpoint: 'menus',
  })
  return response.contents
}

これでメニュー部分においてもmicroCMSで登録したデータを参照して画面の要素が作られるようになりました。

ビルド&デプロイ

大まかに仕組みはできました。
あとは微調整を行い完成度を上げるのと、ビルドとホスティングサービスへのデプロイができれば完了です。

上記のページでは、microCMSのWebhookからGitHub Actionsに連携する方法が記載されています。
このようにすることで、下記のようなワークフローが実現できます。

  • microCMSから更新(Webhook発動)
  • GitHub ActionsでHTMLのビルドとS3へのアップロード

あとは、AWS S3でホスティングするだけです。

まとめ

microCMSを用いた事例を聞く機会が非常に増えており、国内のサービスということもあり今後のさらなる普及も期待出来ます。
実際に手を動かしながら触った感覚としては公式ドキュメントが整備されていること、管理画面も分かりやすく、上手く作られている印象がありました。

ヘッドレスCMSを用いる場合は、microCMSに限らず、今どきのフロントエンド開発の知識が求められるため、少しだけ人を選ぶ技術ではありますが、Vue.jsやReactなどのSPAの何らかの経験とRest API(Web API)の利用経験があれば、今回の記事のようにすんなり導入できるのではないかと思います。

また、セキュリティ要件やそれに付随する技術要件が厳しく、公開するWebサイト一式において動的な処理をしてはいけないこともあるでしょう。
その場合は、今回のようにヘッドレスCMSの利用はあくまでビルドまでの工程として用いて、SSGしてから完全に独立した静的ファイルのみで運用することも可能となります。
こうすれば極端な話、microCMSのサービスが停止しても更新を必要としなければ、作成したWebサイトは稼働を続けられます。

ReactやNext.jsをこれから学ぼうと考えている人にっても、まずは無料で試すことができるのでこれを機会に少し遊んでみてはいかがでしょうか。

  1. キッチンカーをテーマとして選んだことに深い意味はありません。ただ、ザ・ノンフィクションでキッチンカーの話をみて面白かったためです。

  2. v0はNext.jsを開発しているVercel Inc.が提供するReactベースのUI生成サービスです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?