microCMSブログ

microCMSの最新情報や活用事例などをお届けします。

エンジニアリング

microCMSで店舗ごとに出し分けるチラシ配信を設計する

microCMSで店舗ごとに出し分けるチラシ配信を設計する
高宮 竜太 高宮 竜太

こんにちは。microCMSでカスタマーエンジニアをしている高宮です。

店舗のチラシ(PDF)を「全店」「エリア単位」「個別店舗」に出し分けて配信する仕組みを、microCMSとNext.jsで作ってみました。

チラシには、全店で共通のものもあれば、エリアごとのセール、その店舗だけの改装セールもあります。店舗のコンテンツにチラシを持たせる作りにすると、全店に配るチラシを店舗の数だけ登録することになるので、今回はチラシ側に「どこに出すか」を持たせる形にしました。

今回作る仕組み

全店・エリア・個別店舗の配信設定に応じて、各店舗ページに対象のチラシだけが表示されるイメージ

チラシを1件登録すると、指定した時刻に公開され、対象になっている店舗のページにだけ表示されます。

入稿作業は、店舗ページのコンテンツを触らずに、チラシのコンテンツを編集するだけで完結します。

前提

  • フロントエンドにNext.js(App Router)を使います
  • ファイルフィールドを利用するため、Teamプラン以上が対象です(Hobbyプランでの代替案は後述します)

microCMSの設計

APIスキーマ

3つのAPIスキーマを作成し、それぞれをコンテンツ参照フィールドで参照しています。

API

エンドポイント

役割

チラシ

flyers

チラシ本体。PDFと、どこに配信するかを持つ

店舗

shops

店舗マスタ。所属エリアを参照する

エリア

areas

エリアマスタ。参照される側

エリア(areas)

参照されるだけの単純なマスタです。店舗がどのエリアに属するかを設定するためのAPIです。

フィールド

フィールドID

種類

エリア名

name

テキストフィールド

スラッグ

slug

テキストフィールド

店舗(shops)

店舗マスタです。所属するエリアは店舗側から参照するようにしています。

フィールド

フィールドID

種類

店舗名

name

テキストフィールド

住所

address

テキストフィールド

エリア

area

コンテンツ参照 → エリアAPI

チラシ(flyers)

チラシAPIはPDFと配信先を持ちます。フィールドはこの5つです。

フィールド

フィールドID

種類

タイトル

title

テキストフィールド

チラシPDF

pdf

ファイル

配信対象

targetType

セレクトフィールド(all / by_area / by_shop)

対象エリア

targetAreas

複数コンテンツ参照 → エリアAPI

対象店舗

targetShops

複数コンテンツ参照 → 店舗API

対象エリアと対象店舗を「複数コンテンツ参照」で選択できるようにしています。1枚のチラシを複数のエリアや店舗へ配信できる必要があるので、こちらを使います。

入稿方法

表示するPDFを選択する

ファイルフィールドを利用して配信するチラシを選択します。Hobbyプランの場合はファイルフィールドが利用できないため、画像フィールドでの代替となります。

配信対象を選択する

選択肢は「全店(all)」「エリア単位(by_area)」「個別店舗(by_shop)」の3つです。

チラシの配信対象として、全店・エリア単位・個別店舗を選択できる設定画面

配信対象を1つのセレクトフィールドにまとめたので、1枚のチラシで3通りの配り方を表現できます。

配信対象

渋谷店(東京)

新宿店(東京)

横浜店(神奈川)

秋の大感謝祭

全店(all)

表示

表示

表示

東京エリア 週末タイムセール

エリア単位(by_area) → 東京

表示

表示

—

渋谷店 リニューアルオープン

個別店舗(by_shop) → 渋谷店

表示

—

—

表示側は、選ばれた値をそのまま絞り込み条件に使います。選択肢がそのままAPIのレスポンスに載るので、all / by_area / by_shop という、条件にそのまま書ける文字列にしました。

対象エリアを選ぶ

配信対象が by_area の場合、どのエリアに配信するのかを選択します。店舗APIからエリアを参照しているので、エリアを1つ選ぶだけで、そこに属する店舗すべてが配信先になります。

配信対象をエリア単位に設定し、対象エリアとして東京を指定した画面

※ by_area を選んだ上でエリアを未選択のままにすると、そのチラシはどの店舗ページにも表示されません。現在の仕様では他のフィールドの入力状況に合わせてバリデーションを動的に切り替える機能がないため、説明文などを利用して運用側でカバーします。

対象店舗を選ぶ

配信対象が by_shop の場合、どの店舗に配信するのかを選択します。

配信対象を個別店舗に設定し、対象店舗として渋谷店を指定した画面"

※ by_shop を選んだ上で店舗を未選択のままにすると、そのチラシはどの店舗ページにも表示されません。対象エリアと同じく、説明文などを利用して運用側でカバーします。

※ 個別店舗の指定は少数への配信を想定した設計です。指定する店舗が増えると管理コストにもパフォーマンスにも影響するため、エリアのような別のグルーピングを設けることを推奨します。

チラシの公開日時・公開終了日時を予約する

公開開始と公開終了は、スケジュール機能で設定します。

公開開始を指定したチラシはその時刻まで下書きのまま待ち、終了を指定しておけば期間が過ぎたときに下がります。

編集するのはチラシのコンテンツだけです。配信先を何店舗選んでも、店舗やエリアのコンテンツには触れません。

表示側を作る

作る画面は店舗一覧と店舗ページの2つです。店舗一覧は shops を並べるだけなので、ここでは店舗ページを見ていきます。

やることは主に2つで、店舗を1件取って、その店舗に出すチラシを取るだけです。

src/lib/microcms.ts
import { createClient } from "microcms-js-sdk";

export const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,
  apiKey: process.env.MICROCMS_API_KEY!,
});

// 店舗を1件取る。参照している area も一緒に返ってくる
export async function getShop(shopId: string): Promise<Shop> {
  return client.getListDetail<Shop>({ endpoint: "shops", contentId: shopId });
}

// この店舗に出すチラシを、3条件のORで取る
export async function getFlyersForShop(shop: Shop): Promise<Flyer[]> {
  const conditions = [
    `targetType[contains]all`,                 // 全店配布
    `targetShops[contains]${shop.id}`,         // この店舗を名指ししている
  ];
  if (shop.area?.id) {
    conditions.push(`targetAreas[contains]${shop.area.id}`);  // この店舗のエリア
  }

  const res = await client.getList<Flyer>({
    endpoint: "flyers",
    queries: {
      limit: 10,
      filters: conditions.join("[or]"),
      orders: "-publishedAt",
      fields: "id,title,pdf,targetType",
    },
  });
  return res.contents;
}
src/app/shops/[shopId]/page.tsx
export const dynamic = "force-dynamic";

export default async function ShopDetailPage({
  params,
}: {
  params: Promise<{ shopId: string }>;
}) {
  const { shopId } = await params;

  const shop = await getShop(shopId);           // 店舗を1件
  const flyers = await getFlyersForShop(shop);  // この店舗に出すチラシ

  return (
    <>
      <h1>{shop.name}</h1>
      <div className="flyer-grid">
        {flyers.map((flyer) => (
          <FlyerCard key={flyer.id} flyer={flyer} />
        ))}
      </div>
    </>
  );
}

チラシの出し分けのロジックはアプリ側にありません。どのチラシを出すかは、getFlyersForShop が組み立てる絞り込み条件だけで決まります。

ここでは2つの取得を順に待っていますが、店舗名を先に出してチラシだけあとから流し込む、という形にもできます。Suspenseで境界を分けることになり、そのときはチラシ側でも店舗が必要になるので、getShop を React の cache() で包んでおくと、同じ店舗を2回取りに行かずに済みます。

どのチラシを出すかを絞り込む

取得するのは「この店舗に表示すべきチラシ」で、条件は設計そのままの3つのOR条件です。

  1. 配信対象が「全店」
  2. 対象店舗にこの店舗が含まれる
  3. 対象エリアにこの店舗のエリアが含まれる

microCMSの絞り込みは [or] と [contains] を組み合わせてこれを表現できます。さきほどの conditions を join("[or]") でつなぐと、渋谷店を開いたときは次の文字列になります。

filters=targetType[contains]all[or]targetShops[contains]shibuya[or]targetAreas[contains]tokyo

詳しい演算子は APIリクエストのクエリパラメータ と コンテンツの取得(一覧) にまとまっています。

切り替わりは時刻で起きる

公開開始・公開終了の時刻が来ると、microCMS側でコンテンツのステータスが変わります。アプリ側は関与しません。

公開が終了したコンテンツは、コンテンツAPIのレスポンスに含まれません。そのため絞り込み条件に日付は入っていませんし、「まだ掲載期間内か」を判定する処理もありません。返ってきたものをそのまま並べているだけです。

公開終了時刻を過ぎたチラシがコンテンツAPIのレスポンスから除外され、店舗ページから自動的に非表示になるイメージ

キャッシュについては注意です。切り替わりは microCMS側で勝手に起きるイベントなので、アプリにはその瞬間を知る手段がありません。入稿のタイミングで再検証をかけたところで、時刻が来た瞬間には何も起きません。そのため、店舗ページは force-dynamic にして、アクセスのたびに引く形にしています。

ここをISRにするなら、再検証の間隔ぶんだけ切り替えが遅れることを許容するか、Webhook でオンデマンド再検証する設計にすることになります。

注意事項

PDFを扱うにはTeamプラン以上が必要

チラシをPDFで持たせている都合上、この記事の構成はTeamプラン以上が前提です。ファイルアップロードがHobbyプランでは使えません。

Hobbyプランで同じことをやるなら、チラシを画像で持たせる形になります。画像フィールドは全プランで使えますし、紙面を1枚見せるだけなら画像で足ります。表示側も <img> を並べるだけで済みます。

掲載が終わったチラシは残り続ける

公開停止になったチラシは、店舗ページから消えるだけでmicroCMSにはコンテンツ自体は残ります。長く運用すると、管理画面が終了済みのチラシで埋まっていきます。

これは裏を返せば、過去にどのチラシをどの店舗へ出したかが残るということでもあります。掲載履歴として使うなら消さずに残し、アーカイブ用のフィールドを足して一覧から外します。履歴が要らないなら、定期的に削除する運用を決めておきます。

実運用に合わせてさらに機能を拡張するなら

今回の設計では配信の軸はエリアの1つだけ、出し分けは全店・エリア・店舗の3パターンです。ここから先、たとえば次のようなことをやりたくなったら、フィールドを足すことになります。

  • エリア単位の配信から特定の店舗だけ外す:除外用の複数参照フィールドを足し、絞り込みの末尾に除外条件をつなぐ
  • 表示順を決める:優先度の数値フィールドを足して、並び順の指定に加える
  • 配信の軸を増やす:業態別・規模別など、軸が1つ増えるごとに絞り込みのORが1本増える
  • 同じチラシを期間を空けて2回出す:公開予約は1つのコンテンツにつき1セットなので、コンテンツを分ける

また、入稿作業も、1件ずつ登録するぶんには管理画面で対応できます。ただし、複数のチラシを複数の店舗に一括で登録し、それぞれに公開予約まで付けたいとなると、マネジメントAPIを使った独自実装が必要になります。

おわりに

題材はチラシでしたが、あるコンテンツを複数の対象に出し分ける形は、会員ランク別のお知らせ、拠点別の求人、支店ごとの営業案内等でも同じです。

配信するコンテンツを対象側に直接持たせず、独立したAPIから参照させたことで1件の登録が複数の対象に届き、対象側のコンテンツには書き込みが発生しません。

出し分けの軸をいくつ持つか、除外や優先度をどこまで表現するかは、運用する人数や更新の頻度で変わります。この構成をそのまま使うというより、どこまで持たせるかを考えるときの材料にしてもらえればと思います。

microCMS 採用情報

microCMSでは現在、カスタマーエンジニア・プラットフォームエンジニア・プロダクトエンジニアを積極採用中です!

ご興味のある方は、採用情報ページや弊社メンバーが業務内容やチームの雰囲気についてお話しているポッドキャスト「microCMS FM」をぜひチェックしてみてください。

カジュアル面談からでも大歓迎です。お気軽にご応募ください!

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

microCMSは無料ではじめられます。
ご不明な点はお気軽にお問い合わせください。