The commerce module (EC features)
The commerce module is an opt-in e-commerce layer that adds product pages, category listings, keyword search, cart, and checkout. It's designed to be removable: if a site doesn't need EC features, you can delete src/modules/commerce/ and src/pages/[locale]/commerce/ entirely.
The commerce module assumes i18n is opted in.
Directory layout
src/modules/commerce/
blocks/
buy-box/ # "Add to cart" button (used on the PDP)
product-gallery/ # Product image gallery (used on the PDP)
related-products/ # Related products (reuses the cards Block internally)
lib/
cart.ts # Cart state management (localStorage)
favorites.ts # Favorites state management (localStorage, members only)
favorite-button.ts # Shared favorite-button UI logic
acdl-bridge.ts # Converts cart:change into ACDL pushes
locale.ts # Maps route locale to product locale
content/
products/*.yaml # Product data (one file per product)
taxonomy/categories-<locale>.yaml # Category key → display name
src/pages/[locale]/commerce/
index.astro
detail/[slug].astro
products-index.json.ts # JSON index used for search
category/index.astro
category/[category].astro
search.astro
cart/index.astro
cart/checkout/index.astro
confirmation.astro
order.astro
member/index.astro # My Page (only when member.enabled)
member/favorites.astro # Favorites list (only when member.enabled)
Product data
One product = one YAML file, under content/products/<slug>.yaml.
sku: WKDY-SHU-001
images:
- /images/products/commute-running-shoes.png
categories: [shoes]
tags: [running, commute]
stock: in_stock
prices:
ja-JP: { currency: JPY, amount: 18000 }
en-US: { currency: USD, amount: 129.99 }
translations:
ja-JP: { title: "コミュートランニングシューズ", description: "通勤ラン需要に応える軽量設計の..." }
en-US: { title: "Commute Running Shoes", description: "Lightweight shoes built for the run-commute..." }
The schema (the products collection in src/content.config.ts) has these main fields:
sku(string) — the product code, locale-independentimages(string array, at least 1) — image pathscategories(string array, at least 1) — category keys that map to the taxonomy dictionarytags(string array, default[]) — keywords used by searchstock(in_stock|out_of_stock, defaultin_stock)prices({ [locale]: { currency, amount } }) — per-locale pricing, keyed byja-JP/en-UStranslations({ [locale]: { title, description? } }) — per-locale product name and description
SKU, images, category keys, and stock status are locale-independent — only price and copy vary by locale, which keeps the one-file-per-product rule intact.
Run npm run new:product -- <slug> to scaffold a new product file.
Category display names
content/taxonomy/categories-<locale>.yaml maps category keys to their display names.
# categories-ja.yaml
wear: ウェア
shoes: シューズ
bags: バッグ
accessories: アクセサリー
Pages
/<locale>/commerce— commerce home, linking to search, categories, and cart/<locale>/commerce/detail/[slug]— the PDP (product detail page), a fixed Astro template/<locale>/commerce/category— category top, listing every category/<locale>/commerce/category/[category]— a single category's listing, rendered with thecardsBlock/<locale>/commerce/campaign— campaign top and individual campaign pages, written as regular Block-notation pages undercontent/pages/(not a fixed template)/<locale>/commerce/search— keyword search: a static page plus client-side JS/<locale>/commerce/products-index.json— the lightweight JSON index used by search (an API endpoint)/<locale>/commerce/cart— cart contents: list, quantity changes, removal/<locale>/commerce/cart/checkout— a dummy checkout form (no real payment processing)/<locale>/commerce/confirmation— order confirmation; the order isn't placed until you click "place order" here/<locale>/commerce/order— the order-complete (thank-you) page/<locale>/commerce/member— My Page (a hub of member-only links); only generated whenmember.enabledis true insite.config.ts/<locale>/commerce/member/favorites— favorites list and removal; also gated bymember.enabled
The My Page and favorites list live in the commerce module, but the member system itself (login, member issuance) is a separate member module. See Member features (the login sandbox) for details.
The PDP generates the locale × product cross product in getStaticPaths():
export async function getStaticPaths() {
const products = await getCollection('products');
return ['ja', 'en'].flatMap((locale) =>
products.map((product) => ({ params: { locale, slug: product.id }, props: { product } })),
);
}
The cart (cart.ts)
src/modules/commerce/lib/cart.ts manages cart state in localStorage. It has zero dependency on any marketing tool — ACDL integration lives in a separate layer.
type CartItem = {
slug: string; title: string; price: number; currency: 'JPY' | 'USD';
image: string; quantity: number; sku: string; categories: string[];
};
getCart(locale: string): Cart
addItem(locale: string, item: Omit<CartItem, 'quantity'>, quantity?: number): Cart
updateQuantity(locale: string, slug: string, quantity: number): Cart // quantity <= 0 removes the item
removeItem(locale: string, slug: string): Cart
clearCart(locale: string): Cart
- The
localeargument uses product-locale notation (ja-JP/en-US). ThelocalStoragekey isastro-table:cart:<locale>, so each locale gets its own independent cart (this keeps currencies from mixing). - Every mutation dispatches
window.dispatchEvent(new CustomEvent('cart:change', { detail })).detail.actionis one ofadd/update/remove/clear/sync. - To handle multiple open tabs, changes made in another tab are picked up via the
storageevent and re-dispatched withaction: 'sync'.
A typical subscriber, following the pattern used in cart/index.astro:
window.addEventListener('cart:change', (event) => {
if (event.detail.locale === cartLocale) render();
});
ACDL integration (the central bridge)
src/modules/commerce/lib/acdl-bridge.ts subscribes to cart:change and converts it into Adobe Client Data Layer (ACDL) pushes. cart.ts itself has no knowledge that ACDL exists.
cart.ts (state management, vendor-agnostic)
↓ window.dispatchEvent('cart:change', ...)
acdl-bridge.ts (ACDL-specific mapping layer)
↓ window.adobeDataLayer.push({ event: 'add_to_cart', ... })
adobe-client-data-layer
add maps to add_to_cart, remove maps to remove_from_cart, and update looks at the before/after quantity delta to decide which of the two to push. clear and sync push nothing.
acdl-bridge.ts is never auto-loaded — it must be explicitly imported on every page where a cart change can happen (currently commerce/detail/[slug].astro and commerce/cart/index.astro). If you add a new page that mutates the cart, don't forget this import. See Adobe Client Data Layer integration for the full ACDL design.
The buy-box Block
This is the "add to cart" button on the PDP. It calls addItem() from cart.ts, and on success shows a brief visual change plus an "Added to cart" feedback message (aria-live="polite") for a few seconds. Locale-aware price formatting uses Intl.NumberFormat.
Favorites (favorites.ts)
A members-only feature. src/modules/commerce/lib/favorites.ts reads getCurrentMemberId() from the member module (a one-way dependency from commerce to member — the only place in the commerce module that depends on member). When nobody is logged in, addFavorite/removeFavorite return null and refuse to act.
getFavorites(memberId: string): string[]
isFavorite(memberId: string, slug: string): boolean
addFavorite(slug: string): string[] | null // null when nobody is logged in
removeFavorite(slug: string): string[] | null
favorite-button.ts provides the shared UI logic ("only shown when logged in, toggles on click") and is used on the PDP (commerce/detail/[slug].astro). Listing and removing favorites is handled by commerce/member/favorites.astro, positioned under My Page (commerce/member/index.astro).
When the member feature (member.enabled) is off, the pages under commerce/member aren't generated at all, so favorites become effectively unavailable.
Removing the commerce module
If a site doesn't need EC features, delete the following:
src/modules/commerce/src/pages/[locale]/commerce/content/products/andcontent/taxonomy/, then remove theproducts/taxonomycollection registrations fromsrc/content.config.ts- The cart/search/category links in
nav/<locale>.md
The @adobe/adobe-client-data-layer package itself and the ACDL page namespace initialization stay in place — they're core, EC-independent functionality.