React + microCMSで実現するZOZOTOWNキャンペーンページのノーコード化

メインビジュアル画像

はじめに

2020年新卒入社で、現在ZOZOWEB部所属の武井です。ZOZOTOWNのWebフロントエンド開発を担当しています。私は入社以来オフィスに2度しか出社したことがありませんが、そのうちの1度はスタッフインタビュー記事の撮影のときでした。アートがたくさんある素敵なオフィスですが、それ以降出社できていません。まさか新卒1年目からフルリモート勤務をすると思っていませんでしたが、先輩スタッフが仕組み作りをしてくださっていたおかげで快適に働けています。

さて、本題です。ZOZOTOWNではタイムセール、ショップ限定クーポン、抽選プレゼントなどのキャンペーンを期間限定で実施しています。このキャンペーンをより際立たせるためにキャンペーンページを作成し、ホーム画面やメルマガなどを通じてお客様にお届けしています。しかし、このキャンペーンページの作成が必要になった場合、エンジニアが都度実装しており、5日程度の開発工数が発生していました。そこで、このページ作成をビジネスサイドのキャンペーン担当者ができるよう、ノーコード化するシステムを構築しました。本記事では、そこで得たヘッドレスCMSであるmicroCMSとReactに関する知見を紹介します。

背景と課題

ZOZOTOWNのキャンペーンはパンツやシューズなどの商品カテゴリーから、特定の出店ブランドを強調するものまでさまざまな切り口でお届けしています。ZOZOTOWNは多数の商品を取り扱っているため、カテゴリーやブランドを特定し厳選した商品をお客様に提案することで、よりお買い物を楽しめるようにすることがキャンペーンの狙いです。以下にキャンペーンページのイメージを記します。

キャンペーンページのイメージ

アパレル商材は訴求時期が重なるという特徴を持っています。そのため、キャンペーンを短期間に複数開催するということも珍しくありません。理想としては、こうしたキャンペーンを毎日実施し、お客様に日替わりでさまざまな切り口の商品を紹介するサイトにしたいと考えています。また、ZOZOTOWNは先日リニューアルし、コスメ専門モールのZOZOCOSMEや、ラグジュアリー&デザイナーズゾーンのZOZOVILLAがオープンしました。このリニューアルに際し、「シューズ」「コスメ」といったカテゴリーをタブ化しました。これにより、今後はカテゴリー別でキャンペーンをたくさん行う予定です。

しかし、キャンペーンページ作成はエンジニアが個別にマークアップコーディングしています。したがって、毎日続けてキャンペーンページを公開できないのが現実です。

そこで、キャンペーンページ作成時に必要となるエンジニアの作業工数を削減する方法を検討しました。その結果、ビジネスサイドのキャンペーン担当者がコンテンツを編集し、キャンペーンページをノーコードで作成できるシステムを構築することをゴールとしました。

当初はZOZOTOWNの社内サイト管理システムを拡張する形で実現できないか検討を進めました。しかし、リニューアルを進める大規模プロジェクトが並行して走っており、管理システム及びバックエンドの開発リソース確保が難しいという背景がありました。そのため、今回はバックエンドシステムに一切改修を入れずにフロントエンド側だけで完結するシステムを設計しています。

解決方法

早速ですが、まず今回採用したシステム全体図を以下に示します。

システム全体図

以降、この構成にした理由を説明していきます。なお、図中の(1)(2)(3)の番号を説明内で利用するので、上図を適宜参照してください。

microCMS(ヘッドレスCMS)の導入

ノーコード化を実現するために、まずは非エンジニアが操作する管理画面を作成する必要があると考えました。今回の用途のみの管理画面を内製するのは合理的でないため、コンテンツ管理システム、いわゆるCMSの1つであるmicroCMSを導入しました。

CMSといえば、WordPressを思い浮かべる方が多いでしょう。しかし、ZOZOTOWNで導入する場合はヘッドレスCMSが適当だと考えました。「ヘッドレスCMSがそもそも何か」については以下のmicroCMS公式ブログがわかりやすいのでご覧ください。

blog.microcms.io

今回、ヘッドレスCMSを選んだ理由はzozo.jpのWebサーバー上で、このキャンペーンページを動作させたかったためです。キャンペーンページは静的ページではなく、お客様のお気に入り、商品のパーソナライズなども行います。この機能をWordPressなどの別サーバーに構築した場合、バックエンドの開発工数の発生が予想されます。その点、APIで連携するヘッドレスCMSであれば、現在利用しているテクノロジーを変えることなく部分的にCMS機能を使うことが可能です。また、CSSやJavaScriptの実行環境もこれまで通りに維持できるため、フロントエンド資産の流用、連携も可能です。

なお、ヘッドレスCMSはmicroCMS以外にも、ContentfulStrapiなど多数存在します。また、WordPressもプラグインなどを駆使してヘッドレスCMSとして利用できます。しかし、今回のCMSのニーズを以下のように整理したところ、すべて満たすものはmicroCMSのみでした。

  • コンテンツはエンジニアではないビジネスサイドの担当者が編集するので、管理画面でメタタグなども含めたほぼすべてのコンテンツを編集可能にしたい
  • パターン化されたコンテンツを並び替えたり、複数設定できるようにしたい
  • 編集結果を本番と同じ見た目で確認できるプレビューページを用意したい
  • プレビューページはリリースするキャンペーンページと同じ環境で動作させたい
  • インフラの構築や運用作業を不要にしたい
  • システム利用者の役割ごとに適切なコンテンツの編集権限管理がしたい
  • 日本語対応したい

加えて、microCMSはお客様ニーズの勘所を抑えている機能を続々とリリースしている印象がありました。また、公開されているロードマップも便利そうな機能が並んでいました。このことから、今後も改善され続けるだろうという期待を込めて、microCMSを選定した側面もあります。microCMSのブログで、過去の新機能リリースをみていただけるとお分かりいただけるかもしれません。

blog.microcms.io

以上がmicroCMSを導入した理由です。

microCMSを用いた管理画面とAPIの構築

次に、システム全体図の(1)でmicroCMSが実現している管理画面とAPIの構築について説明します。

microCMSではコンテンツ入力項目の最小単位をフィールドと呼びます。フィールドには次のような種類のデータ形式1が設定できます。

microCMSで設定できるフィールドのデータ形式

これらのフィールドを組み合わせて管理画面を構築していきます。今回作成するキャンペーンページのコンテンツモデルは先述の通り、コンテンツを並び替えたり、複数設定できる柔軟なものである必要があります。microCMSには繰り返しフィールドとカスタムフィールドと呼ばれる機能があり、それらを利用することで実現可能です。

この機能についてはmicroCMSのブログ、「microCMSのカスタムフィールドを使ってランディングページを作ろう」で詳しく解説されています。また、サービスの作成や管理画面の操作方法などの基本的な解説は、公式ブログやmicroCMS の公式ドキュメントに委ね、省略します。実際にドキュメントを読みながら操作したところ、つまずく点は特にありませんでした。

blog.microcms.io

document.microcms.io

最終的にキャンペーンページのコンテンツモデル(APIスキーマ)の設定は以下の形式になります。

キャンペーンページのAPIスキーマ

また、上記の設定から構築される管理画面は以下の通りです。

キャンペーンページ作成時の管理画面

この管理画面上でそれぞれのコンテンツを入力すると、JSON APIエンドポイントが作成されます。

例えば、本記事の冒頭で挙げたキャンペーンページのイメージの場合、以下のJSONを返します。

{
  "id": "sample_campaign", // UUIDが発行される。手動で変更も可能
  "createdAt": "2020-12-31T15:00:00.000Z", // microCMSの時刻表記形式は ISO8601
  "updatedAt": "2020-12-31T15:00:00.000Z",
  "publishedAt": "2020-12-31T15:00:00.000Z",
  "revisedAt": "2020-12-31T15:00:00.000Z",
  "managedTitle": "(テスト作成中)キャンペーンページサンプル",
  "displays": [ // 「フィールドを追加」をクリックするとカスタムフィールドを選ぶモーダルが掲出する。コンテンツを選べば入力フォームが現れ編集ができる。(繰り返しフィールド)
    {
        "fieldId": "mainVisual", // カスタムフィールドには設定したIDが自動付与される
        "title": "Brand DAY",
        "lead": "Brand の 2日間限定のクーポン&タイムセール開催中!",
        "backgroundImagePC": {
            "url": "https://images.microcms-assets.io/assets/**/pc_mv.jpg",
            "height": 1000,
            "width": 2560
        },
        "backgroundImageSP": {
            "url": "https://images.microcms-assets.io/assets/**/sp_mv.jpg",
            "height": 750,
            "width": 750
        }
    },
    {
        "fieldId": "favoriteGoods",
        "allShopID": 0, // お客様のお気に入り商品を取得する内部APIのリクエストに必要なパラメーターショップID
        "menShopID": 1, // お客様の性別が判別される場合は別のショップIDを設定できるように設定
        "womenShopID": 2,
        "kidsShopID": 3,
    },
    {
        "fieldId": "searchMenu",
        "title": "人気カテゴリー",
        "searchMenuItems": [
            {
                "fieldId": "searchMenuItem",
                "title": "Tシャツ",
                "url": "/search/xxxx"
            },
            {
                "fieldId": "searchMenuItem",
                "title": "ボトムス",
                "url": "/search/xxxx"
            },
            ...
        ]
    },
    {
        "fieldId": "goodsCatalog",
        "tagID": 0, // キャンペーンごとにtagID社内管理ツールを用いて商品をタグで紐付けをすることができる。内部WebAPIのリクエストパラメーターに用いる
        "isCoupon": true, // クーポン商品のみ絞り込むかのフラグ。内部WebAPIのリクエストパラメーターに用いる
        "title": "スペシャルクーポン",
        "subTitle": "最大¥1,000分のクーポン発行中",
        "url": "/search/xxxx" // 「すべてをアイテムをみる」の遷移先のリンク
    },
  ],
  "meta": { // HTMLのメタタグ関連の設定
    "fieldId": "meta",
    "title": "メタタイトル",
    "description": "メタ詳細"
  },
  "campaignDate": { // キャンペーンの期間設定、これに応じてページの公開・非公開を制御する
    "fieldId": "campaignDate",
    "startDate": "2020-12-31T15:00:00.000Z",
    "endDate": "2021-01-01T15:00:00.000Z"
  }
}

このようにCMSのコンテンツはすべてJSON APIで取得できるので、さまざなプログラミング言語や環境から呼び出すことができます。主に利用するのはコンテンツIDからコンテンツ情報を取得するAPIと、エンドポイントのコンテンツすべてを配列で取得するAPIの2つです。読み込みだけではなく書き込みも可能なので、他システムとの連携も柔軟にできるでしょう。より詳細は以下のAPIドキュメントを参照ください。

https://document.microcms.io/content-api/get-list-contents/document.microcms.io

キャンペーンページ表示コンテンツのReactコンポーネント化

次に、APIのレスポンスの中でも、ページに表示させるコンテンツ部分を説明します。先ほどのJSONの中にあるdisplaysという配列に注目してください。

"displays": [
    {
        "fieldId": "mainVisual",
        ...
    },
    {
        "fieldId": "favoriteGoods",
        ...
    },
    {
        "fieldId": "searchMenu",
        ...
    },
    {
        "fieldId": "goodsCatalog",
        ...
    }
  ]

このdisplays繰り返しフィールドを利用しています。この機能で表示コンテンツを並び替えたり、複数設定できるような操作を可能としています。フィールドとUIコンポーネントを1対1で対応させ、これらを組み合わせることでページを作成します。

キャンペーンページとフィールドの関係

ここで紹介しているmainVisual,favoriteGoods,searchMenu,goodsCatalogの他にも計14点のコンポーネントを定義しました。これらのフィールドを組み合わせることで多様なキャンペーンページの作成が可能です。

ソースコード上では、これらをReactコンポーネントで定義しています。また、フィールドはTypeScriptで型定義しているので、型安全にコンポーネントを管理できます。例えば、mainVisualフィールドであれば、以下のようなReactコンポーネントと型定義をセットで記述します。

// microCMSで定義できるフィールドを定義
interface MicroCMSField {
  text: string
  textArea: string
  image: {
    url: string
    height: string
    width: string
  }
  ...
}

interface MicroCMSCustomField<T, U> {
  fieldId: T
} & Partial<U>

// microCMSで定義したカスタムフィールドのIDを定義
const CUSTOM_FIELD = {
  mainVisual: 'mainVisual',
  ...
} as const

type MainVisualField = MicroCMSCustomField<
  typeof CUSTOM_FIELD.mainVisual,
  {
    title: MicroCMSField['text']
    lead: MicroCMSField['textArea']
    backgroundImagePC: MicroCMSField['image']
    backgroundImageSP: MicroCMSField['image']
  }
>

interface Props {
  field: MainVisualField
  device: Device
}

import React, { FC } from 'react'

export const MainVisual: FC<Props> = ({ field, device }) => {
  const {
    title
    lead
    backgroundImagePC
    backgroundImageSP
  } = field
  const isPC = device === 'pc'

  return (
    <section>
      <Title>{title}</Title>
      <Lead>{lead}</Lead>
      {isPC ? (
        <BackgroundImagePC src={backgroundImagePC} />
      ) : (
        <BackgroundImageSP src={backgroundImageSP} />
      )}
    </section>
  )
}

また、mainVisualのフィールドはmicroCMSの設定画面では以下のように定義しています。

メインビジュアルのカスタムフィールド定義

このフィールド変更時に型定義も変更するようにしています。イメージとしてはRDBのスキーマ更新に近いかもしれません。フィールドとコンポーネント定義を対応させることで、フィールド変更時、ソースコードに不整合が発生しないか型検証が可能です。この検証により、型の不整合を未然に防ぎ、コンポーネントを安全に改修できました。

システム全体図の(1)microCMSの説明は以上です。

プレビューURLの発行とプレビューページの実装

次にCMS上の編集結果を確認するプレビューページについて説明します。

システム全体図の(2)プレビューページの部分です。microCMSのメニューに「画面プレビュー」というボタンがあります。このボタンを押した際の遷移先URLをどのような形式で発行するかを、以下のように設定できます。

プレビューURLの発行の設定

コンテンツ編集者はボタンからプレビューページに遷移できます。

遷移先のプレビューページは関係者のみが閲覧可能なパイロット環境に構築しています。このパイロット環境はzozo.jp本番環境と同じデータベースに接続しています。そのため、この環境で内部APIをリクエストすれば、本番と同じ商品データを取得ができます。これはキャンペーンで紹介したい実商品データなどを取得してプレビューしたかった事情もあります。

これにより、ビジネスサイドのキャンペーン担当者は公開されるページと商品情報なども含めて全く同一の見た目でプレビュー確認できる状態が実現できました。

プレビューページと公開されるページの見た目は同じです。しかし、実装の中身は異なります。プレビューページでは閲覧の度にmicroCMSのAPIをリクエストし、そのレスポンスからDOMを生成しています。いわゆるCSR(クライアントサイドレンダリング)と呼ばれるレンダリング手法です。一方、公開されるページは、静的なマークアップに変換してリリースする形を取りました。以降、この理由を説明します。

Webページのレンダリングに関する説明は、以下の記事が参考になります。

developers.google.com

この記事でも、以下のようにお客様体験を良くしたい場合はSSRか静的レンダリングが推奨されています。2

かいつまんで言うと、私たちは開発者が完全なリハイドレーションの上で、サーバーレンダリングまたは静的レンダリングを検討することを勧めるでしょう。

また、お客様体験の向上以外にも、CSRを採用しない理由に通信コストの問題があります。CSRではmicroCMSに都度データフェッチをするため、データ転送が発生します。microCMSはデータ転送量に応じた従量課金制なため、コストの観点からもCSRは望ましくありませんでした。

では、本番にリリースするページをどのように静的なマークアップに変換しているのかを次節で説明します。

キャンペーンページのリリース

本番にリリースするページについて説明します。システム全体図の(3)に該当します。

ヘッドレスCMSを用いたシステムを構築する場合、リリースするページはNext.jsGatsbyなどのJamstackに対応したフレームワークを導入し、SSGやISR3の機能を利用するのが一般的なセオリーと言われています。

しかし、ZOZOTOWNではテンプレートエンジンのようなミドルウェア4でマークアップを記述しており、今回はその記法に対応させる必要がありました。したがって、今回はこのようなフレームワークを導入せず、ReactのReactDOMServer.renderToStaticMarkupを駆使して静的なマークアップに変換するCLIツールをNode.jsで実装しました。5このツールを社内ではjsx2markupと呼んでいます。jsx2markupにより、プレビューページとリリースページはレンダリング手法が異なっていても、同じJSXのコードベースを用いることが可能になります。

jsx2markupは、以下のようなコマンドを叩くことでリリース用のマークアップファイルを生成し、リリースします。6

ts-node --files -r tsconfig-paths/register ./jsx2markup --endpointName="microCMSのエンドポイント名" --contentID="microCMSの管理画面で設定したコンテンツID"

jsx2markupの詳細は省きますが、先ほど言及したテンプレート記法に変換する処理など、ZOZOTOWNの環境に対応させるためのさまざまな処理をしています。加えて、画像URLをmicroCMSのものからZOZOTOWNで普段利用している画像サーバーのURLに変更する処理なども行っています。

この変換は以下のような関数で記述しました。

import path from "path";

const getURLBasename = (url: string): string =>
  path.basename(new URL(url).pathname);

// APIのレスポンスの型をジェネリクスで渡す
export const transformFromCMSImages = <T>({
  contents,
  targetDirectory,
}: {
  contents: T;
  targetDirectory: string;
}): T => {
  const microCMSImageUrlRegex = /https?:\/\/images.microcms-assets.io[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#\u3000-\u30FE\u4E00-\u9FA0\uFF01-\uFFE3]+/gm;
  const stringContents = JSON.stringify(contents);
  const matches = stringContents.match(microCMSImageUrlRegex) || [];
  const microCMSImageUrls = Array.from(matches);
  return JSON.parse(
    microCMSImageUrls.reduce(
      (acc, _, i) =>
        acc.replace(
          microCMSImageUrls[i],
          `${targetDirectory}/${getURLBasename(microCMSImageUrls[i])}`
        ),
      stringContents
    )
  );
};

この処理は、画像をmicroCMSによってアップロードされたものではなく、社内で利用している画像サーバーにホスティングしたかったため必要でした。このようなニーズは、ZOZOTOWN以外でもあり得る要求だと思うので、参考になれば幸いです。

リリースは、基本的にはエンジニアがコマンドを叩くだけです。一手間かかりますが、静的なマークアップに変換することで別のメリットも得られました。

そのメリットとは、マークアップに追加の変更を加えることができる点です。なぜならば、少しのデザイン変更を加えることで、多種多様な出店ブランドの世界観やキャンペーンの訴求力を高められることがあるからです。こうしたケースへの対応も、完全に自動化してしまうと対応が困難になります。しかし、静的なマークアップに変換してしまえば、変換後のマークアップに変更を加えてリリースすることで対応可能です。そのため、リリースはあえて自動化していません。

以上で構築した環境の説明は終わります。

効果

ソフトウェアだけ作っても運用がうまくされなければ意味がありません。このキャンペーンページの場合もオペレーションを含めて考えなければいけませんでした。

そのため、スムーズな運用ができるようにビジネスサイドと定期的に議論し、CMSのマニュアル作成やキャンペーンの実施フローなどもこれを機に見直しました。

その結果、4月のシステム導入から、本記事を公開した5月14日に至るまでに、8個のページをリリースできました。これは昨年比で2倍の数です。特にゴールデンウィーク中は毎日キャンペーンを実施し、5個のページをリリースできました。

これまでの仕組みでは短期間に複数のページをマークアップすることが困難でしたが、その課題を解決でき、有用なシステムを作れたと手応えを感じています。

また、エンジニアではないビジネスサイドの担当者でも、CMS上で編集しながらプレビュー確認が行えるようになったため、デザイナーの工数削減やページの手戻りが発生しづらくなったという効果もありました。

今後もシステムや業務フローを洗練させ、キャンペーン数を増やし、お客様にお買い物を楽しんでいただけるようなサービスにしていきます。

まとめ

今回はZOZOTOWNのキャンペーンページ作成をキャンペーン担当者ができるよう、ノーコード化するシステムの構築手法、そこで得たmicroCMSやReactに関する知見や効果について紹介しました。

ZOZOTOWNは、JavaScriptに関してはES5, jQueryからReact, TypeScriptに移行中です。今後はさらにフロントエンドのWebサーバーのリプレイスも予定しています。

今回のシステムも、そのリプレイスを見据え、サーバー移行しやすいように設計しました。リプレイスで本システムにも変化がありましたら、またご紹介します。

最後に

私はZOZOTOWNを担当するエンジニアになって1年が経過しましたが、その独特なシステムにいまだ衝撃を受ける毎日です。

約16年前に誕生し、凄まじい勢いで成長してきたZOZOTOWNは、その当時としては優れたミドルウェアや技術で構築されており、現在の規模までスケールさせた先輩スタッフには尊敬の念しかありません。

しかし、これらの技術は進化の激しいソフトウェア開発の世界では、現在ではいわゆる技術的負債と呼ばれるものとなり、開発速度を鈍化させる要因の1つになっていることは否めません。今回作ったシステムも、その負債の制約がなければ、もっと別のやり方があったでしょう。しかし、これを私はネガティブには捉えていません。なぜならば、ZOZOTOWNは技術的な改善により、まだまだ伸び代があるということを意味しているからです。

ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、下記のリンクからぜひご応募ください。

tech.zozo.com

また、ZOZOTOWNのフロントエンドエンジニアに興味がある方は森口にDMをいただいても構いません。よろしくお願いします!


  1. 記事を執筆した2021年5月時点の情報です。また、料金プランによって選択できるデータ形式は異なります。

  2. Webは進化し続けるので2021年現在でも同じ結論になるとは限りません。しかし、検証コストやサーバーに改修を入れられない都合上、プレビューページと実際にお客様に届けるページのレンダリング手法を別にすることは、システムの構想段階から想定していました。

  3. Jamstack, SSG, ISRについてはご存知ない方はこちらの記事が参考になります。

  4. RubyにおけるHaml、JavaScriptにおけるPugのようなもので、VBScriptで利用できるマークアップ記法です。

  5. 余談ですが、Reactはフレームワークというよりシンプルなライブラリであろうとする思想があるため、このような小回りが効くが強力なAPIがある点は素晴らしいと改めて思いました。

  6. tsconfig-paths/registerを用いることで、このスクリプトとクライアントサイドのTypeScriptの設定ファイルを共通化できます。この説明のために、本記事ではあえてコマンド上に露出させていますが、実際はnpm scriptsでエイリアスを当てています。

カテゴリー