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. See Adobe Client Data Layer integration for the payload details (the productListItems array, commerce.cart.cartID, etc.).
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.