
【第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 テストします。
今回は以上です。