メインコンテンツへスキップ
見出し画像
Photo bymirecat

【第619回】Personalization:トップページに中央強調型カルーセルを表示する

    これまでの記事では、TrailNest の顧客の興味を Affinity で数値化し、Hero Banner やおすすめ商品の表示に活用してきました。

    今回は、トップページのおすすめ商品を、中央の商品を大きく見せるカルーセルに変更します。

    画像

    中央にある商品を強調し、左右には前後の商品を表示します。矢印やスワイプで切り替えることで、次の商品が中央に移動する構成です。

    この回では、まず「全カテゴリの人気商品を最大 10 商品」を表示します。そして、次回の記事では、そのカルーセルを使って、全カテゴリの人気商品と、顧客が最も興味を持っているカテゴリの人気商品を A/B テストします。


    Personalization 基礎シリーズ

    Sitemap Builder シリーズ

    Data 360 設定シリーズ

    Personalization 設定シリーズ


    ① 今回の構成

    今回も、商品を選ぶ役割と、商品を表示する役割を分けます。

    • Recommender:過去 2 週間の商品詳細ページの閲覧数を基に、商品を選ぶ。

    • Personalization Point:おすすめ商品の取得先を用意し、最大返却件数を設定する。

    • Decision:使用する Recommender を指定する。

    • Experience Template:商品の画像・名前・価格・リンクをカードとして表示する。

    • CloudPages の JavaScript:中央の商品を切り替え、矢印操作などを制御する。

    • Web Personalization Manager:トップページの表示場所に、Experience Template を適用する。

    今回は、商品の取得を WPM に任せます。CloudPages に追加する JavaScript は、カルーセルの操作を担当します。


    今回変更する場所

    今回の対象は、トップページのレコメンドです。

    現在は以下の通り、シンプルな API 方式による表示になっています。

    画像
    • トップページ:現在の API 方式による表示から、WPM の中央強調型カルーセルへ切り替える。

    • 商品詳細ページ:現在の WPM レコメンドを継続する。

    • 注文完了ページ:現在の API 形式のレコメンドを継続する。

    • Hero Banner:現在の設定を継続する。

    既存の Recommender や Experience Template を直接変更すると、他の表示にも影響する可能性があります。そのため、今回の表示に使用する設定は新規で作成します。


    ② 全カテゴリの人気商品を返す Recommender を作成する

    Personalization アプリで Recommenders タブを開きます。

    以前作成した TrailNest Category Popular Recommendations を参考に、今回使用する Recommender を新規作成します。

    • Recommender Name:TrailNest Home All Popular

    • Recommender API Name:TrailNest_Home_All_Popular

    • Profile Data Graph:TrailNest Profile

    • Item Data Graph:TrailNest Item Data Graph

    • Fallback Recommender:無効

    • Recommendation Type:Rule-Based Recommendations

    画像

    商品の並び順には、以前の記事で設定した、過去 2 週間の商品詳細ページの閲覧数を使用します。

    • 並べ替え項目:既存の人気商品(商品詳細ページの閲覧数が多い順の意味)用 Calculated Insight の閲覧数

    • Sort Order:Descending(降順)

    画像

    今回は全カテゴリから商品を選ぶため、Affinity に基づくカテゴリのフィルター条件は設定しません。


    ③ トップページ用の Personalization Point を作成する

    Personalization Points タブを開き、新規作成します。

    • Profile Data Graph:Real-Time > TrailNest Profile

    • Name:TrailNest Home Carousel

    • API Name:TrailNest_Home_Carousel

    • Personalization Type:Recommendations

    • Content Schema:TrailNest Category Recommendation Content

    • Maximum Number of Recommendations to Return:10

    • Authentication Required:未チェック

    今回は、以前に作った既存の商品レコメンド用 Content Schema を再利用します。

    Recommendations を選択した場合に、最大返却件数を設定できます。 今回の 10 商品は、この Personalization Point 側で指定します。

    画像

    「10 商品」と「画面に見える商品数」は別です。

    10 商品を一度に画面へ並べるのではなく、中央の商品と、その左右の商品を見せます。操作すると、後ろに続く商品が表示されます。


    ④ 通常配信用の Decision を作成する

    作成した Point に、次の Decision を追加します。

    • Decision Name:TrailNest Home Popular Default

    • Decision API Name:TrailNest_Home_Popular_Default

    • Decision State:Live

    • Recommender:TrailNest Home All Popular

    • Targeting Rules:Always

    画像

    この段階では、訪問者に全カテゴリの人気商品を表示します。

    次回は、この Point に Experiment を設定し、実験の対象となる顧客を 2 つのグループに分けます。


    ⑤ 中央強調型カルーセルの Experience Template を作成する

    Experience Templates タブで、新しいテンプレートを作成します。

    • Channel:Web

    • Name:TrailNest Centered Product Carousel

    • API Name:TrailNest_Centered_Product_Carousel

    画像
    • Connected Websites:TrailNest の Website Connector

    • Template Type:Handlebars

    • Creation Method : Start from Scratch

    • Content Schema:TrailNest Category Recommendation Content

    商品詳細ページで使用しているレコメンドテンプレートと同じように、次の変数を商品データに対応付けます。

    • image:商品の画像 URL

    • url:商品詳細ページの URL

    • name:商品名

    • price:数値の価格

    これらは、以下の HTML の subVar で参照します。

    画像

    Turn off auto-engagement tracking はチェックしません。

    Experience Template に貼り付けるコード

    以下は、商品の見た目を定義するコードです。

    <style>
    .tn-center-recs {
      --tn-green: #234b35;
      --tn-ink: #183426;
      --tn-card-width: min(78%, 380px);
      --tn-edge: 20px;
    
      display: block;
      box-sizing: border-box;
      width: 100%;
      min-width: 0;
      max-width: 1200px;
      margin: 0 auto;
      padding: 12px 0 24px;
      color: var(--tn-ink);
      font-family: Arial, Helvetica, sans-serif;
    }
    
    .tn-center-recs *,
    .tn-center-recs *::before,
    .tn-center-recs *::after {
      box-sizing: border-box;
    }
    
    .tn-center-recs__intro {
      margin: 0 0 12px;
      text-align: center;
    }
    
    .tn-center-recs__eyebrow {
      margin: 0 0 10px;
      color: var(--tn-green);
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 2.5px;
    }
    
    .tn-center-recs__description {
      margin: 0;
      color: #647369;
      font-size: 15px;
      line-height: 1.6;
    }
    
    .tn-center-recs__track {
      display: flex;
      align-items: stretch;
      gap: 20px;
      width: 100%;
      min-width: 0;
      margin: 0;
      padding: 24px var(--tn-edge) 34px;
      overflow-x: auto;
      overscroll-behavior-x: contain;
      scroll-snap-type: x mandatory;
      scrollbar-width: none;
    }
    
    .tn-center-recs__track::-webkit-scrollbar {
      display: none;
    }
    
    .tn-center-recs__item {
      flex: 0 0 var(--tn-card-width);
      min-width: 0;
      margin: 0;
      scroll-snap-align: center;
    }
    
    .tn-center-recs__card {
      display: flex;
      flex-direction: column;
      height: 100%;
      overflow: hidden;
      border: 1px solid #e0e7dc;
      border-radius: 22px;
      background: #fff;
      transform: scale(.88);
      transform-origin: center;
      box-shadow: 0 3px 12px rgba(24, 52, 38, .035);
      transition:
        transform .28s ease,
        border-color .28s ease,
        box-shadow .28s ease;
    }
    
    .tn-center-recs__item.is-active .tn-center-recs__card {
      transform: scale(1);
      border-color: #a6ba9d;
      box-shadow:
        0 18px 32px rgba(24, 52, 38, .10),
        0 3px 8px rgba(24, 52, 38, .04);
    }
    
    .tn-center-recs__image-link {
      display: flex;
      align-items: center;
      justify-content: center;
      aspect-ratio: 4 / 3;
      padding: 20px;
      background: #f3f6ee;
    }
    
    .tn-center-recs__image {
      display: block;
      width: 100%;
      height: 100%;
      min-height: 0;
      max-width: 100%;
      object-fit: contain;
      user-select: none;
      -webkit-user-drag: none;
    }
    
    .tn-center-recs__body {
      display: flex;
      flex: 1;
      flex-direction: column;
      padding: 22px;
    }
    
    .tn-center-recs__label {
      margin: 0 0 10px;
      color: #6d806b;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: 1.8px;
    }
    
    .tn-center-recs__name {
      margin: 0 0 18px;
      color: var(--tn-ink);
      font-size: 22px;
      line-height: 1.3;
      letter-spacing: -.4px;
      overflow-wrap: anywhere;
    }
    
    .tn-center-recs__price {
      margin: auto 0 22px;
      color: var(--tn-green);
      font-size: 25px;
      font-weight: 700;
    }
    
    .tn-center-recs__cta {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
      min-height: 46px;
      padding: 13px 19px;
      border-radius: 999px;
      background: var(--tn-green);
      color: #fff;
      text-decoration: none;
      font-size: 13px;
      font-weight: 700;
    }
    
    .tn-center-recs__cta:hover {
      background: #183c29;
    }
    
    .tn-center-recs__controls {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 20px;
      margin-top: 0;
    }
    
    .tn-center-recs__controls[hidden] {
      display: none;
    }
    
    .tn-center-recs__button {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 48px;
      height: 48px;
      padding: 0;
      border: 1px solid #d6e0d0;
      border-radius: 50%;
      background: #fff;
      color: var(--tn-green);
      font: inherit;
      font-size: 22px;
      cursor: pointer;
      box-shadow: 0 3px 10px rgba(24, 52, 38, .04);
    }
    
    .tn-center-recs__button:hover:not(:disabled) {
      border-color: var(--tn-green);
      background: var(--tn-green);
      color: #fff;
    }
    
    .tn-center-recs__button:disabled {
      opacity: .35;
      cursor: default;
    }
    
    .tn-center-recs__status {
      min-width: 64px;
      color: #647369;
      text-align: center;
      font-size: 13px;
      font-variant-numeric: tabular-nums;
    }
    
    .tn-center-recs a:focus-visible,
    .tn-center-recs button:focus-visible,
    .tn-center-recs__track:focus-visible {
      outline: 3px solid #b17c37;
      outline-offset: -3px;
    }
    
    @media (max-width: 600px) {
      .tn-center-recs__track {
        gap: 12px;
      }
    
      .tn-center-recs__card {
        transform: scale(.92);
      }
    
      .tn-center-recs__body {
        padding: 18px;
      }
    
      .tn-center-recs__name {
        font-size: 19px;
      }
    
      .tn-center-recs__price {
        font-size: 23px;
      }
    
      .tn-center-recs__description {
        padding: 0 18px;
        font-size: 13px;
      }
    }
    
    @media (prefers-reduced-motion: reduce) {
      .tn-center-recs__card {
        transition: none;
      }
    }
    </style>
    
    {{#if data.length}}
    <div
      class="tn-center-recs"
      data-tn-center-carousel
      role="region"
      aria-label="TrailNest recommended products"
      aria-roledescription="carousel"
    >
      <div class="tn-center-recs__intro">
        <p class="tn-center-recs__eyebrow">
          TRAILNEST · YOUR NEXT ADVENTURE
        </p>
    
        <p class="tn-center-recs__description">
          Explore the gear. Find your next favorite.
        </p>
      </div>
    
      <div
        class="tn-center-recs__track"
        role="list"
        tabindex="0"
        aria-label="Recommended products. Scroll to explore."
      >
        {{#each data}}
          <div
            class="tn-center-recs__item"
            role="listitem"
          >
            <article class="tn-center-recs__card">
              <a
                class="tn-center-recs__image-link"
                href="{{subVar 'url'}}"
                aria-label="{{subVar 'name'}}"
                draggable="false"
              >
                <img
                  class="tn-center-recs__image"
                  src="{{subVar 'image'}}"
                  alt=""
                  loading="lazy"
                  decoding="async"
                  draggable="false"
                />
              </a>
    
              <div class="tn-center-recs__body">
                <p class="tn-center-recs__label">
                  TRAILNEST GEAR
                </p>
    
                <h3 class="tn-center-recs__name">
                  {{subVar 'name'}}
                </h3>
    
                <p class="tn-center-recs__price">
                  ¥ <span data-tn-price>{{subVar 'price'}}</span>
                </p>
    
                <a
                  class="tn-center-recs__cta"
                  href="{{subVar 'url'}}"
                  draggable="false"
                >
                  <span>Explore this gear</span>
                  <span aria-hidden="true">↗</span>
                </a>
              </div>
            </article>
          </div>
        {{/each}}
      </div>
    
      <div
        class="tn-center-recs__controls"
        hidden
      >
        <button
          class="tn-center-recs__button"
          type="button"
          data-tn-prev
          aria-label="Previous product"
        >
          ←
        </button>
    
        <span
          class="tn-center-recs__status"
          data-tn-status
          role="status"
          aria-live="polite"
          aria-atomic="true"
        ></span>
    
        <button
          class="tn-center-recs__button"
          type="button"
          data-tn-next
          aria-label="Next product"
        >
          →
        </button>
      </div>
    </div>
    {{/if}}
    画像

    Save & Activate で有効にします。

    商品ページへ移動する場所

    今回、商品ページへのリンクは、次の 2 か所に限定しています。

    • 商品画像

    • Explore this gear ボタン

    商品名や価格の部分はリンクにしていません。

    モバイルで横にスワイプしようとした際に、誤って商品ページへ移動することを減らすためです。


    ⑥ CloudPages にカルーセルの操作処理を追加する

    次のコードを、CloudPages の </body> の直前に追加します。

    これはカルーセルの動きを制御する処理です。

    • 中央に近い商品を強調する。

    • 左右のボタンで商品を切り替える。

    • 画面幅に合わせてカードの大きさを調整する。

    • WPM が後から商品を表示した場合にも初期化する。

    • 数値の価格を、小数点以下なし・桁区切りありで表示する。

    • スワイプ直後の誤クリックを抑制する。

    このコードは、今回の専用クラスを持つカルーセルだけを対象にします。

    <script>
    (function () {
      if (window.__trailNestCenterCarouselInstalled) return;
      window.__trailNestCenterCarouselInstalled = true;
    
      const instances = new Map();
    
      const reducedMotion = window.matchMedia(
        "(prefers-reduced-motion: reduce)"
      );
    
      function setup(root) {
        const track = root.querySelector(
          ".tn-center-recs__track"
        );
    
        const items = Array.from(
          root.querySelectorAll(".tn-center-recs__item")
        );
    
        const previous = root.querySelector("[data-tn-prev]");
        const next = root.querySelector("[data-tn-next]");
        const status = root.querySelector("[data-tn-status]");
    
        const controls = root.querySelector(
          ".tn-center-recs__controls"
        );
    
        if (
          !track ||
          !items.length ||
          !previous ||
          !next ||
          !status ||
          !controls
        ) {
          return null;
        }
    
        // Start with the second product centered when possible.
        // The first product remains visible on the left.
        let activeIndex = items.length >= 3 ? 1 : 0;
    
        let frame = 0;
        let resizeFrame = 0;
        let pointerStart = null;
        let moved = false;
        let blockClickUntil = 0;
    
        // Format numeric JPY prices without decimal places.
        root.querySelectorAll("[data-tn-price]").forEach(
          function (node) {
            const raw = node.textContent.trim();
    
            if (!/^-?\d+(\.\d+)?$/.test(raw)) return;
    
            const amount = Number(raw);
            if (!Number.isFinite(amount)) return;
    
            node.textContent = new Intl.NumberFormat("ja-JP", {
              maximumFractionDigits: 0
            }).format(amount);
          }
        );
    
        function paint(index) {
          activeIndex = index;
    
          items.forEach(function (item, itemIndex) {
            item.classList.toggle(
              "is-active",
              itemIndex === index
            );
          });
    
          previous.disabled = index === 0;
          next.disabled = index === items.length - 1;
    
          const label = (index + 1) + " / " + items.length;
    
          if (status.textContent !== label) {
            status.textContent = label;
          }
        }
    
        function nearestIndex() {
          const trackRect = track.getBoundingClientRect();
          const center = trackRect.left + track.clientWidth / 2;
    
          let nearest = 0;
          let distance = Infinity;
    
          items.forEach(function (item, index) {
            const rect = item.getBoundingClientRect();
    
            const current = Math.abs(
              rect.left + rect.width / 2 - center
            );
    
            if (current < distance) {
              nearest = index;
              distance = current;
            }
          });
    
          return nearest;
        }
    
        function goTo(index, animate) {
          const target = Math.max(
            0,
            Math.min(items.length - 1, index)
          );
    
          const trackRect = track.getBoundingClientRect();
          const itemRect = items[target].getBoundingClientRect();
    
          const left =
            track.scrollLeft +
            itemRect.left +
            itemRect.width / 2 -
            trackRect.left -
            track.clientWidth / 2;
    
          const smooth = animate && !reducedMotion.matches;
    
          track.scrollTo({
            left: left,
            behavior: smooth ? "smooth" : "auto"
          });
    
          if (!smooth) {
            paint(target);
          }
        }
    
        function layout() {
          const width = track.clientWidth;
          if (!width) return;
    
          const cardWidth = Math.min(
            380,
            width * (width < 640 ? 0.8 : 0.44)
          );
    
          root.style.setProperty(
            "--tn-card-width",
            cardWidth + "px"
          );
    
          root.style.setProperty(
            "--tn-edge",
            Math.max(0, (width - cardWidth) / 2) + "px"
          );
    
          goTo(activeIndex, false);
        }
    
        // Highlight the product closest to the center.
        track.addEventListener(
          "scroll",
          function () {
            if (frame) return;
    
            frame = requestAnimationFrame(function () {
              frame = 0;
              paint(nearestIndex());
            });
          },
          { passive: true }
        );
    
        previous.addEventListener("click", function () {
          goTo(nearestIndex() - 1, true);
        });
    
        next.addEventListener("click", function () {
          goTo(nearestIndex() + 1, true);
        });
    
        // Keyboard navigation when the track itself is focused.
        track.addEventListener("keydown", function (event) {
          if (event.target !== track) return;
    
          if (event.key === "ArrowLeft") {
            event.preventDefault();
            goTo(nearestIndex() - 1, true);
          } else if (event.key === "ArrowRight") {
            event.preventDefault();
            goTo(nearestIndex() + 1, true);
          } else if (event.key === "Home") {
            event.preventDefault();
            goTo(0, true);
          } else if (event.key === "End") {
            event.preventDefault();
            goTo(items.length - 1, true);
          }
        });
    
        // Reduce accidental navigation after a swipe.
        track.addEventListener(
          "pointerdown",
          function (event) {
            pointerStart = {
              x: event.clientX,
              y: event.clientY
            };
    
            moved = false;
            blockClickUntil = 0;
          },
          { passive: true }
        );
    
        track.addEventListener(
          "pointermove",
          function (event) {
            if (!pointerStart) return;
    
            const distance = Math.hypot(
              event.clientX - pointerStart.x,
              event.clientY - pointerStart.y
            );
    
            if (distance > 10) {
              moved = true;
            }
          },
          { passive: true }
        );
    
        function finishPointer(event) {
          if (moved || event.type === "pointercancel") {
            blockClickUntil = performance.now() + 500;
          }
    
          pointerStart = null;
        }
    
        track.addEventListener(
          "pointerup",
          finishPointer,
          { passive: true }
        );
    
        track.addEventListener(
          "pointercancel",
          finishPointer,
          { passive: true }
        );
    
        track.addEventListener(
          "click",
          function (event) {
            if (
              event.target.closest("a") &&
              performance.now() < blockClickUntil
            ) {
              event.preventDefault();
              event.stopImmediatePropagation();
            }
          },
          true
        );
    
        controls.hidden = items.length < 2;
        paint(activeIndex);
    
        const resizeObserver = new ResizeObserver(function () {
          cancelAnimationFrame(resizeFrame);
    
          resizeFrame = requestAnimationFrame(layout);
        });
    
        resizeObserver.observe(track);
        layout();
    
        return function cleanup() {
          resizeObserver.disconnect();
          cancelAnimationFrame(frame);
          cancelAnimationFrame(resizeFrame);
        };
      }
    
      function scan() {
        // Release observers for content removed by WPM.
        instances.forEach(function (cleanup, root) {
          if (!root.isConnected) {
            cleanup();
            instances.delete(root);
          }
        });
    
        // Initialize newly rendered WPM carousels.
        document.querySelectorAll(
          "[data-tn-center-carousel]"
        ).forEach(function (root) {
          if (instances.has(root)) return;
    
          const cleanup = setup(root);
    
          if (cleanup) {
            instances.set(root, cleanup);
          }
        });
      }
    
      function start() {
        const scope = document.getElementById("trailnest-app");
        if (!scope) return;
    
        const observer = new MutationObserver(scan);
    
        observer.observe(scope, {
          childList: true,
          subtree: true
        });
    
        scan();
      }
    
      if (document.readyState === "loading") {
        document.addEventListener(
          "DOMContentLoaded",
          start,
          { once: true }
        );
      } else {
        start();
      }
    })();
    </script>

    この処理では、カルーセルを無限に循環させるための商品カードの複製は行いません。

    最初の商品では左ボタン、最後の商品では右ボタンを無効にします。Recommender が返した商品と、その並び順をそのまま使います。


    ⑦ 通常の表示を維持したまま、確認用の表示場所を用意する

    トップページには、現在の API レコメンド用の表示場所があります。

    <div id="home_recs" class="recommendation-zone" hidden></div>

    まずは、この要素を残したまま、直後に次の確認用コードを追加します。

    既存の API レコメンドの JavaScript より前に配置してください。

    <script>
    (function () {
      const params = new URLSearchParams(location.search);
    
      if (params.get("tn_carousel_preview") !== "1") return;
    
      const slot = document.getElementById("home_recs");
      if (!slot) return;
    
      slot.id = "home_wpm_recs";
      slot.className = "tn-home-wpm-zone";
      slot.hidden = false;
    })();
    </script>

    このコードは、URL に tn_carousel_preview=1 がある場合だけ、トップページの表示場所を WPM 用に切り替えます。

    • 通常の URL:これまでの API レコメンドを表示する。

    • 確認用の URL:WPM のカルーセルを確認する。

    これにより、通常の訪問者には現在のレコメンドを表示しながら、新しい表示を確認できます。

    以前の記事の API コードは、対象の表示場所がなければ処理を終了する構成でした。そのため、確認用ページでは #home_recs がなくなり、トップページの API レコメンドは動作しません。

    これは公開前の確認用の切り替えです。WPM が失敗した場合に API へ戻すフォールバック処理ではありません。

    確認用 URL

    トップページの URL に、次のパラメーターを付けます。

    ?tn_carousel_preview=1&sf_personalization_wpm

    すでに Query String がある場合は、? ではなく & で追加してください。

    また、商品詳細ページの URL ではなく、Page Type が Home になる URLを使用します。


    ⑧ Web Personalization Manager で設定する

    確認用 URL でトップページを開き、Web Personalization Manager から新しい Experience を作成します。

    • Experience:Embedded Content

    • Personalization Point:TrailNest Home Carousel

    • Experience Template:TrailNest Centered Product Carousel

    • State:Disabled

    画像

    Location では、次のように設定します。

    • 表示対象:Current Page Type(Home)

    • Display Method:Replace an Element

    • Selector:#home_wpm_recs

    画像

    Content の設定は、次のとおりです。

    • Engagement Destination:Product Engagement

    画像

    ⑨ プレビューで表示と操作を確認する

    Preview Settings を開きます。

    まずは、今回作成した TrailNest Home Popular Default を選択して、Preview を実行します。

    画像

    確認する内容は、次のとおりです。

    • 全カテゴリの人気商品が返っている。

    • 商品候補が十分にあれば、最大 10 商品が表示される。

    • 左右のボタンで、次の商品を中央に移動できる。

    • モバイルでは、横にスワイプできる。

    • 商品名と価格の部分をタップしても、商品ページへ移動しない。

    • 画像と Explore this gear ボタンから、正しい商品ページへ移動できる。

    • 価格が円建てで表示される。

    • ページ全体に横スクロールが発生しない。

    続いて Current User Decision を選択し、通常の Decision 評価でも表示されることを確認します。

    画像

    今回は通常配信用の Decision が Always のため、同じ条件であれば全カテゴリの人気商品が表示されます。


    ⑩ トップページを WPM 方式へ切り替える

    確認が終わったら、通常のトップページも WPM 方式へ切り替えます。

    先に WPM の設定を有効にする

    確認用 URL で作成した Experience の State を Enabled にし、Save します。

    画像

    この時点では、通常のトップページには、まだ #home_wpm_recs がありません。通常の訪問者には、これまでの API 用の表示場所が残っています。


    CloudPages の表示場所を変更する

    トップページの次の HTML を探します。

    <div id="home_recs" class="recommendation-zone" hidden></div>

    これを、次の HTML に置き換えます。

    <div id="home_wpm_recs" class="tn-home-wpm-zone"></div>

    続いて、⑦で追加した tn_carousel_preview を判定する確認用コードを削除します。以下のものを削除です。

    <script>
    (function () {
      const params = new URLSearchParams(location.search);
    
      if (params.get("tn_carousel_preview") !== "1") return;
    
      const slot = document.getElementById("home_recs");
      if (!slot) return;
    
      slot.id = "home_wpm_recs";
      slot.className = "tn-home-wpm-zone";
      slot.hidden = false;
    })();
    </script>

    最後に、CloudPages を公開します。

    これで、通常の URL でも WPM 用の表示場所が使用されます。

    新しい表示場所には hidden を付けません。 今回は、API 用コードが表示後に hidden を解除する構成ではなく、WPM が表示を担当するためです。


    API コード側のトップページ指定を外す

    以前の記事の API コードに、次のような指定が残っている場合は、

    home: "#home_recs",

    この行を削除します。

    商品詳細ページをすでに WPM に切り替えており、注文完了ページだけを API で表示する構成なら、対象の指定は次のようになります。

    const selectors = {
      order_confirmation: "#order_confirmation_recs_row_1"
    };

    注文完了ページで使用する API 処理(order_confirmation)は残します。


    通常の URL で最終確認する

    確認用パラメーターを付けずにトップページを開き直します。

    画像
    • 中央強調型カルーセルが表示される。

    • 旧 API レコメンドと二重に表示されない。

    • Hero Banner が表示される。

    • 商品詳細ページの WPM レコメンドが表示される。

    • 注文完了ページの API レコメンドが維持されている。

    CloudPages の更新が反映されたことを確認して、切り替え完了です。


    次回は、商品の選び方を A/B テストする

    今回のカルーセルは、全カテゴリの人気商品を表示しています。

    次回は、この表示を使って、次の 2 つを比較します。

    • A:全カテゴリから選んだ人気商品
      過去 2 週間の商品詳細ページの閲覧数で並べ、最大 10 商品を表示します。

    • B:最も興味があるカテゴリの人気商品
      Affinity が最も高いカテゴリに絞り、そのカテゴリ内の閲覧数で並べます。現在の TrailNest の品ぞろえでは、最大 5 商品です。
      ※ 各カテゴリにつき、5 商品しか登録がないためです。

    カルーセルのデザインや操作方法は共通にします。

    今回は件数も異なるため、幅広いカテゴリから紹介する体験と、興味のあるカテゴリに絞って紹介する体験の比較として扱います。


    いかがでしたでしょうか。

    今回は、TrailNest のトップページに、中央の商品を強調するカルーセルを作成しました。

    商品の選択は Recommender、見た目は Experience Template、操作は CloudPages の JavaScript が担当します。WPM でこれらをトップページの表示場所につなぐことで、おすすめ商品をリッチに見せる構成にしました。

    次回は、このカルーセルを使って、人気商品と顧客の興味に合う商品を A/B テストします。

    今回は以上です。


    次の記事はこちら

    前回の記事はこちら

    私の note のトップページはこちら

     
     
    Salesforce Marketing Cloud、Agentforce、Data Cloud、Salesforce 認定資格に関する実践的な情報を発信しています。これらの記事が、皆さまの学習や日々の業務に少しでもお役に立てば幸いです。