SalesforceのLWR (Lightning Web Runtime) で構築する、架空の英国紅茶ブランドサイト 「The Royal Brew」。
前回の記事では、標準機能とCSSハックでトップページを作成しました。
今回は、ECサイトの要である 「商品詳細ページ」 の開発です。
ブランドの世界観を完璧に表現するため、標準の「レコードの詳細」コンポーネントではなく LWC を開発してゼロから実装することにしました。
参考:今回のサンプルコード
クリックしてLWCのコードを展開する
1. royalProductDetail.js-meta.xml
LWRサイトで recordId を受け取るための重要な設定ファイルです。
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Royal Product Detail</masterLabel>
<description>商品詳細を表示するLWR用コンポーネント</description>
<targets>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
<targetConfigs>
<targetConfig targets="lightningCommunity__Default">
<property
name="recordId"
type="String"
label="Record ID"
description="The record ID from the URL."
default="{!recordId}"
/>
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
2. royalProductDetail.js
CMS画像のパス解決ロジックと、データの取得ロジックです。
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import basePath from '@salesforce/community/basePath'; // サイトのベースパス
// 商品オブジェクトの項目(環境に合わせてAPI名を変更してください)
import NAME_FIELD from '@salesforce/schema/Product2.Name';
import CODE_FIELD from '@salesforce/schema/Product2.ProductCode';
import DESC_FIELD from '@salesforce/schema/Product2.Description';
import FAMILY_FIELD from '@salesforce/schema/Product2.Family';
import IMAGE_FIELD from '@salesforce/schema/Product2.DisplayUrl';
import PRICE_FIELD from '@salesforce/schema/Product2.Price__c';
const FIELDS = [NAME_FIELD, CODE_FIELD, DESC_FIELD, FAMILY_FIELD, IMAGE_FIELD, PRICE_FIELD];
export default class RoyalProductDetail extends LightningElement {
@api recordId; // meta.xmlの設定により、ここにURLのIDが入ります
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
product;
// --- Getters ---
get name() {
return getFieldValue(this.product.data, NAME_FIELD);
}
get productCode() {
return getFieldValue(this.product.data, CODE_FIELD);
}
get description() {
return getFieldValue(this.product.data, DESC_FIELD);
}
get family() {
return getFieldValue(this.product.data, FAMILY_FIELD);
}
// CMS画像のパス問題を解決するゲッター
get imageUrl() {
const rawUrl = getFieldValue(this.product.data, IMAGE_FIELD);
if (!rawUrl) {
return 'https://via.placeholder.com/600x600?text=No+Image';
}
// サイトのプレフィックス(basePath)と /sfsites/c を結合
const sitePrefix = basePath === '/' ? '' : basePath;
return `${sitePrefix}/sfsites/c${rawUrl}`;
}
// 通貨フォーマット
get price() {
const rawPrice = getFieldValue(this.product.data, PRICE_FIELD);
if (!rawPrice) return '---';
return new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(rawPrice);
}
get isLoading() {
return !this.product.data;
}
}
3. royalProductDetail.css
Styling Hooks(CSS変数)を使用した、テーマ変更に強いCSS設計です。 ※事前にエクスペリエンスビルダーで :root 定義を行っている前提とします。
/* 全体のコンテナ: 変数がない場合のフォールバック色も指定 */
.royal-pdp-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
font-family: 'EB Garamond', serif;
color: var(--royal-navy, #0F1F3D);
}
/* 2カラムレイアウト */
.product-layout {
display: flex;
gap: 4rem;
align-items: flex-start;
flex-wrap: wrap;
}
/* 左カラム: 画像 */
.image-column {
flex: 1;
min-width: 300px;
}
.image-wrapper {
background: #fff;
padding: 2rem;
border: 1px solid var(--royal-border, #E0E0E0);
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
display: flex;
justify-content: center;
align-items: center;
}
.image-wrapper img {
max-width: 100%;
height: auto;
max-height: 500px;
object-fit: contain;
}
/* 右カラム: 情報 */
.info-column {
flex: 1;
min-width: 300px;
}
.product-category {
font-family: 'Cinzel Decorative', cursive;
color: var(--royal-gold, #C5A059);
font-size: 1.1rem;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
text-transform: uppercase;
font-weight: 700;
}
.product-title {
font-family: 'Cinzel Decorative', cursive;
font-size: 3rem;
line-height: 1.2;
margin-bottom: 0.5rem;
color: var(--royal-navy, #0F1F3D);
}
.product-description {
font-size: 1.1rem;
line-height: 1.8;
margin-bottom: 2rem;
color: #444;
}
/* 価格とアクション */
.action-area {
margin-top: 2rem;
border-top: 1px solid var(--royal-border, #eee);
padding-top: 2rem;
}
.price-tag {
font-family: 'Cinzel Decorative', cursive;
font-size: 2.2rem;
color: var(--royal-navy, #0F1F3D);
margin-bottom: 1.5rem;
}
.price-tag .tax {
font-size: 1rem;
color: var(--royal-text-sub, #888);
font-family: 'Lato', sans-serif;
}
/* カートボタン: 変数によるテーマ適用 */
.add-to-cart-btn {
background-color: var(--royal-navy, #0F1F3D);
color: #fff;
border: none;
padding: 1rem 3rem;
font-family: 'Cinzel Decorative', cursive;
font-size: 1.2rem;
letter-spacing: 0.1em;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
font-weight: 700;
}
.add-to-cart-btn:hover {
background-color: var(--royal-gold, #C5A059);
color: var(--royal-navy, #0F1F3D);
}
4. royalProductDetail.html
構造の骨組みです。
<template>
<div class="royal-pdp-container">
<template lwc:if={isLoading}>
<lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
</template>
<template lwc:else>
<div class="product-layout">
<div class="image-column">
<div class="image-wrapper">
<img src={imageUrl} alt={name} />
</div>
</div>
<div class="info-column">
<div class="product-header">
<p class="product-category">{family}</p>
<h1 class="product-title">{name}</h1>
<p class="product-code">Item Code: {productCode}</p>
</div>
<div class="product-description">
<p>{description}</p>
</div>
<div class="action-area">
<p class="price-tag">{price} <span class="tax">(Tax incl.)</span></p>
<button class="add-to-cart-btn">Add to Cart</button>
</div>
</div>
</div>
</template>
</div>
</template>
今回の実装ポイント:LWRの流儀に従う
LWRサイトは、従来のAuraサイトとは異なり、Web標準技術(Shadow DOMなど)に厳格に準拠しています。そのため、開発のアプローチも変える必要があります。
- recordIdの取得: LWRサイト特有の「プロパティバインド設定」。
- CMS画像連携: 外部ユーザーにも画像を表示させるパス解決ロジック。
- Styling Hooks: 「Shadow DOMの壁」を越えてデザインを管理する設計。
Step 1: LWRで recordId を受け取る「お作法」
まず最初の関門です。社内用(Lightning Experience)のLWCでは @api recordId だけで動きましたが、LWRサイトでは動作しません。 js-meta.xml での明示的な設定 が必要です。
<targetConfigs>
<targetConfig targets="lightningCommunity__Default">
<property
name="recordId"
type="String"
label="Record ID"
default="{!recordId}"
/>
</targetConfig>
</targetConfigs>
この default="{!recordId}" が重要です。これにより、ビルダーがURL(例: /product/01t...)からIDを抽出し、コンポーネントに渡してくれるようになります。

Step 2: CMS画像のパス問題を解決する
次に商品画像です。DisplayUrl 項目などにCMSの画像パス(/cms/delivery/media/...)が入っている場合、そのままでは表示されません。 サイトのパスプレフィックス と LWR特有のパス を結合する必要があります。

// royalProductDetail.js
import basePath from '@salesforce/community/basePath'; // サイトのベースパス
const FIELDS = [..., IMAGE_FIELD]; // フィールド定義
// ...
@api recordId;
@wire(getRecord, { recordId: '$recordId', fields: FIELDS })
product;
get imageUrl() {
const rawUrl = getFieldValue(this.product.data, IMAGE_FIELD);
if (!rawUrl) return '';
// basePath (例: /theroyalbrew) と /sfsites/c を結合
const sitePrefix = basePath === '/' ? '' : basePath;
return `${sitePrefix}/sfsites/c${rawUrl}`;
}
この処理を挟むことで、最終的に https://...site.com/theroyalbrew/sfsites/c/cms/delivery/media/MC... というURLになりCMS画像が取得できます。
Step 3: Styling Hooks で「色」を管理する
ここが今回のハイライトです。 LWRでは、Shadow DOMによってコンポーネントの内部スタイルが外部から隔離されています。そのため、「外部CSSで無理やり色を変える」という古い手法は通用しません。
代わりに、Salesforceが提供する Styling Hooks(CSSカスタムプロパティ) の仕組みを活用します。
戦略:標準フック vs 独自フック
通常は、Salesforce標準の --dxp-g-brand(テーマパネルのブランド色)などを使うのがセオリーです。 しかし今回は、「The Royal Brew」専用の厳密なカラーパレット(Royal Navy & Gold)を定義したいため、あえて 「独自のStyling Hooks」 を設計します。
グローバル変数の定義
エクスペリエンスビルダーの左側のパネルから [設定] > [詳細] > [ヘッドマークアップを編集] で、サイト全体のパレットを定義します。これが「色の水源」になります。
<style>
:root {
/* The Royal Palette */
--royal-gold: #C5A059; /* アクセント、ボタン、枠線 */
--royal-navy: #0F1F3D; /* メインテキスト、見出し、ボタン背景 */
--royal-ivory: #FDFBF7; /* 背景色 */
--royal-text-sub: #888888; /* 補足テキスト、パンくず */
--royal-border: #E0E0E0; /* 薄い境界線 */
}
</style>
もし、ビルダーの「テーマパネル」の色設定と連動させたい場合は、--royal-gold: var(--dxp-g-brand); のようにマッピングすることも可能です。
LWCでの使用(安全策付き)
LWC側では、直接カラーコードを書かずに変数を使います。
/* royalProductDetail.css */
.product-title {
/* var(変数名, フォールバック色) */
/* 変数が読み込めない時のために第2引数を指定するのがベストプラクティス */
color: var(--royal-navy, #0F1F3D);
font-family: 'Cinzel Decorative', cursive;
}
.add-to-cart-btn {
background-color: var(--royal-navy, #0F1F3D);
color: #fff;
}
.add-to-cart-btn:hover {
/* ホバー時は変数を切り替えるだけ */
background-color: var(--royal-gold, #C5A059);
}
このように実装することで、将来「ゴールドの色味を少し変えたい」となった場合でも、LWCのコードを一切触ることなく、ビルダー上のCSSを1行修正するだけでサイト全体に反映されます。
完成画面
標準機能の制約から解放され、かつ保守性も高い「商品詳細ページ」が完成しました。

次回の制作日誌
デザインは完成しました。次は「機能」です。 「カートに入れる」ボタンを押した時、ヘッダーのカートアイコンの数字をどうやって増やすのか? コンポーネント間の通信技術 「Lightning Message Service (LMS)」 の実装に挑みます。





読者の声