Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Deco Skills

A collection of reusable, battle-tested patterns and best practices for building fast, maintainable storefronts with Deco on TanStack Start, React, and VTEX.

## Skills

### [Configurable On-Demand Minicart](./deco-minicart-configuravel.md)
Build an API-frugal, CMS-configurable VTEX minicart with React Query. Zero `getOrCreateCart` on page load, lazy orderForm creation, canonical Minicart shape, micro-skeletons, and toast-vs-drawer toggle.

**Reference:** `montecarlo-tanstack`

### [Slim Add-to-Cart (Fetch Inteligente)](./deco-add-to-cart-slim.md)
Optimize add-to-cart bandwidth from 97 KB → 0.3 KB by returning only essential data on add, deferring full cart hydration to drawer-open intent.

**Benefit:** ~99.7% bandwidth reduction, no duplicate cart fetches.

### [Signal Reactivity in React (Preact→React Migration Gotcha)](./deco-signal-reactivity-react.md)
Critical migration gotcha: reading `signal.value` in render doesn't re-render in React. Use `useSignalValue` hook instead.

**Symptom:** Drawer/modal doesn't open on click, but analytics logs fire.

### [Micro-Skeletons Without Layout Shift](./deco-micro-skeletons.md)
Implement fine-grained loading states per line/section using pulse-in-place (not fixed boxes) to preserve exact dimensions and avoid CLS violations.

**Pattern:** Disable the real widget, don't hide it.

### [Navigation Prefetch (HTML + SPA)](./deco-nav-prefetch.md)
Combine HTML prefetch-on-hover (nav links, using bagaggio/instant.page) with SPA Link wrapper prefetch (product cards) for perceived performance gains.

**Benefit:** Category pages prefetch on nav hover; PDPs prefetch on card hover.

## Using These Skills

1. Pick a skill relevant to your use case.
2. Read the full skill document for context, gotchas, and trade-offs.
3. Copy the reference implementation patterns into your project.
4. Follow the verification checklist to validate the integration.

## Reference Implementations

All skills are tested in production at:
- **montecarlo-tanstack** — TanStack Start + React + VTEX, Deco framework

## Contributing

Found a gotcha not documented here? Have a better pattern? Open a PR or discussion — these skills are living docs.
85 changes: 85 additions & 0 deletions skills/deco-add-to-cart-slim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: Slim Add-to-Cart (Fetch Inteligente)
description: Optimize add-to-cart bandwidth by returning only essential data (~0.3KB) on add, deferring full cart hydration to drawer-open intent.
tags: [performance, add-to-cart, fetch, optimization]
---

# Slim Add-to-Cart (Fetch Inteligente)

## Problem
Traditional add-to-cart returns the full VTEX OrderForm (~97 KB) on every add, even though the browser only needs: `orderFormId`, item count, and total. This causes bandwidth waste and network latency spikes.

## Solution
Create a slim server function that runs the add server-side but returns only `{ orderFormId, itemCount, totalQuantity, value }` (~0.3 KB). The full OrderForm is fetched on-demand only when the drawer opens.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Correct the bandwidth terminology across the guide and catalog.

The implementation trims the add-to-cart response by omitting the full OrderForm; it does not make the request itself 97 KB → 0.3 KB. Update both documents to distinguish response payload size from total network traffic.

  • skills/deco-add-to-cart-slim.md#L13-L13: describe the returned response as slim rather than claiming general bandwidth reduction.
  • skills/deco-add-to-cart-slim.md#L75-L76: qualify the 97 KB → 0.3 KB metric as a response-payload comparison.
  • skills/deco-add-to-cart-slim.md#L84-L85: verify response size separately from request size.
  • skills/README.md#L12-L15: mirror the corrected terminology in the catalog.
📍 Affects 2 files
  • skills/deco-add-to-cart-slim.md#L13-L13 (this comment)
  • skills/deco-add-to-cart-slim.md#L75-L76
  • skills/deco-add-to-cart-slim.md#L84-L85
  • skills/README.md#L12-L15
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/deco-add-to-cart-slim.md` at line 13, Correct the terminology at
skills/deco-add-to-cart-slim.md lines 13, 75-76, and 84-85, and skills/README.md
lines 12-15: describe the optimization as reducing the add-to-cart response
payload, qualify the 97 KB to 0.3 KB figure as a response-size comparison, and
distinguish response-size verification from request-size measurement; mirror
this terminology in the catalog.


## Implementation

### 1. Server Function (Slim Add)
```ts
// src/server/invoke.ts
import { addItemsToCart } from "@decocms/apps-vtex/actions/checkout";

export interface SlimCartResult {
orderFormId: string;
itemCount: number;
totalQuantity: number;
value: number;
}

const _addItemsToCartSlim = createServerFn({ method: "POST" })
Comment on lines +19 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect surrounding lines.
git ls-files 'skills/deco-add-to-cart-slim.md'
wc -l skills/deco-add-to-cart-slim.md
cat -n skills/deco-add-to-cart-slim.md | sed -n '1,140p'

# Search for the referenced API usage/imports in the repo docs.
rg -n "createServerFn|addItemsToCartSlim|SlimCartResult|inputValidator|orderFormId" skills . --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**'

Repository: deco-cx/deco

Length of output: 7244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect nearby documentation files that might define the snippet style.
fd -a -t f 'deco-add-to-cart-slim.md|deco-add-to-cart' . || true

Repository: deco-cx/deco

Length of output: 201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect other skills docs for snippet conventions around imports/abbreviations.
for f in skills/*.md; do
  printf '\n== %s ==\n' "$f"
  rg -n "omitted|abbreviated|import .*createServerFn|// src/server/invoke.ts|```" "$f" || true
done | sed -n '1,260p'

Repository: deco-cx/deco

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for explicit guidance or examples showing whether imports are omitted in code snippets.
rg -n "omitted imports|abbreviated|copy-paste|snippet|```ts|```js|```tsx|```jsx" skills/*.md | sed -n '1,220p'

# Sample a few nearby skills docs for code-block style around imports.
for f in skills/*.md; do
  echo "== $f =="
  sed -n '1,80p' "$f" | rg -n "^import |^// |^```|^# |^## " || true
done | sed -n '1,260p'

Repository: deco-cx/deco

Length of output: 453


Import createServerFn or mark the snippet as abbreviated.
As written, the server-function example references createServerFn without showing its import, so it isn’t copy-paste runnable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/deco-add-to-cart-slim.md` around lines 19 - 29, Update the
_addItemsToCartSlim example in skills/deco-add-to-cart-slim.md to import
createServerFn from its established package so the snippet is copy-paste
runnable, or explicitly mark the snippet as abbreviated if the import is
intentionally omitted.

.inputValidator((data: {
orderFormId: string;
orderItems: Array<{ id: string; seller: string; quantity: number }>;
}) => data)
Comment on lines +30 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the referenced skill and any related action/docs.
git ls-files | rg '^skills/deco-add-to-cart-slim\.md$|deco-add-to-cart|add-to-cart'

# Show the referenced section with line numbers.
if [ -f skills/deco-add-to-cart-slim.md ]; then
  echo "---- skills/deco-add-to-cart-slim.md ----"
  cat -n skills/deco-add-to-cart-slim.md | sed -n '1,120p'
fi

# Search for the validator and any downstream payload validation/contract docs.
rg -n --hidden --glob '!**/.git/**' \
  'inputValidator\(\(data: \{|\borderFormId\b|\borderItems\b|seller: string|quantity: number|deco-add-to-cart-slim' \
  skills . 2>/dev/null || true

Repository: deco-cx/deco

Length of output: 7413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the add-to-cart action implementation or type definitions.
rg -n --hidden --glob '!**/.git/**' \
  'addItemsToCart|actions/checkout|orderItems: Array<\{ id: string; seller: string; quantity: number \}>' \
  . 2>/dev/null || true

# If there is a likely action file, show the relevant sections with line numbers.
for f in $(git ls-files | rg '(^|/)(checkout|actions|server|sdk).*\.(ts|tsx|js|md)$' || true); do
  if rg -q 'addItemsToCart|orderItems|seller|quantity' "$f"; then
    echo "---- $f ----"
    cat -n "$f" | sed -n '1,220p'
    echo
  fi
done

Repository: deco-cx/deco

Length of output: 1458


🌐 Web query:

TanStack Start createServerFn inputValidator runtime validation identity function semantics

💡 Result:

In TanStack Start, the inputValidator method (often used interchangeably with validator) is used to perform runtime validation and data transformation on the input passed to a createServerFn before it reaches the handler [1][2][3]. Core semantics and runtime behavior: 1. Runtime Validation: The inputValidator accepts a validation schema (such as Zod, Valibot, or ArkType via Standard Schema) or a custom function [1][2][3]. If validation fails, it throws an error, preventing the handler from executing [4][2][3]. 2. Identity Function Semantics: When a raw function is passed as an inputValidator, it acts as an identity or transformation function [2][3]. It receives the input unknown, and the returned value is provided to the handler as the validated data [1][2]. If the function does not explicitly validate the input (e.g., throwing on invalid types), it essentially treats the input as trusted, which is generally discouraged for security [2]. 3. Supported Validator Shapes: The underlying execution logic (execValidator) supports multiple patterns [3]: - Standard Schema: Objects containing a ~standard property (e.g., Zod, Valibot) [3]. - Parseable Objects: Objects with a parse method (e.g., older Zod versions) [3]. - Functions: A simple callback function, which provides the identity/transformation semantics mentioned above [3]. 4. Error Handling: Current implementations of inputValidator generally throw a generic Error when validation fails [3]. Note that in some versions, this can cause structured validation errors (like those from Zod) to lose their metadata during serialization across the network boundary, as they are often stringified into the error message [4]. For developers, it is recommended to use Standard Schema-compatible libraries (like Zod) to ensure both type-safe inference and robust runtime validation [1][2]. While custom identity functions are supported, they do not provide automatic type narrowing or validation unless manually implemented within the function body [2].

Citations:


Validate the add-to-cart payload at the server boundary. inputValidator((data) => data) is a pass-through, so this endpoint still accepts arbitrary client input. Add schema checks for orderFormId, id, seller, and quantity > 0, or move that contract into addItemsToCart and document it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/deco-add-to-cart-slim.md` around lines 30 - 33, Replace the
pass-through validator in the add-to-cart endpoint with server-side schema
validation for orderFormId, each order item’s id and seller, and a strictly
positive quantity. Keep the validated payload shape aligned with addItemsToCart,
or move the same contract into addItemsToCart and document the boundary.

.handler(async ({ data }): Promise<SlimCartResult> => {
// VTEX returns full OrderForm; we extract only what the browser needs.
const of = await addItemsToCart(data);
const items = of?.items ?? [];
return {
orderFormId: of?.orderFormId ?? data.orderFormId,
itemCount: items.length,
totalQuantity: items.reduce((s, it) => s + (it?.quantity ?? 0), 0),
value: of?.value ?? 0,
};
});
```

### 2. Query Hook (On-Demand Gate)
```ts
// src/sdk/cart/useCartQuery.ts
const addItemsMutation = useMutation({
mutationFn: async (params: { orderItems: Array<{ id: string; seller: string; quantity: number }> }) => {
markCartMutated();
const orderFormId = await ensureOrderForm();
return invoke.vtex.actions.addItemsToCartSlim({ data: { orderFormId, orderItems: params.orderItems } });
},
onSuccess: (slim) => {
// Slim result: only write cookie + badge, don't hydrate full cart here.
if (slim?.orderFormId) writeOrderFormCookie(slim.orderFormId);
writeCartCount(slim?.itemCount ?? 0);
// Invalidate so the next drawer-open (when enabled becomes true) refetches authoritative data.
queryClient.invalidateQueries({ queryKey: cartKeys.all });
},
});
```

### 3. Fetch Gate (Intent + Cookie)
```ts
// src/sdk/cart/queries.ts
export function shouldFetchCart(displayCartIntent: boolean): boolean {
return displayCartIntent && (Boolean(readOrderFormCookie()) || _mutationRanThisSession);
}
```

## Benefits
- **Add bandwidth:** 97 KB → 0.3 KB (~99.7% reduction)
- **No duplicate getOrCreateCart:** The old gate's `mutationRan` term caused the full cart to fetch immediately after add. New gate defers to drawer-open intent.
- **Badge works without full hydration:** `cart_item_count` cookie keeps header badge updated.

## Trade-offs
- Full OrderForm is fetched later (on drawer open), not immediately. This is intentional — the add user usually doesn't open the drawer immediately.
- A user who adds then immediately opens the drawer will see a brief skeleton while fetching. This is acceptable UX.

## Verification
- **Network:** Add-to-cart request is ~0.3 KB; opening drawer triggers the full orderForm fetch.
- **Correctness:** Quantity changes and subsequent adds work correctly with stale data handling.
99 changes: 99 additions & 0 deletions skills/deco-micro-skeletons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: Micro-Skeletons Without Layout Shift
description: Implement fine-grained loading states per line and section without causing visual collapse or layout thrashing.
tags: [ux, skeletons, performance, css, loading-states]
---

# Micro-Skeletons Without Layout Shift

## Problem
Traditional skeleton screens that swap out the real content with fixed-size placeholder boxes cause **layout shift** (CLS violation) and visual jarring. Example: a cart line's price block is 2 lines when discounted (strikethrough + final price), but the skeleton is 1 line — the row collapses during fetch.

## Solution
**Pulse the real content in place** instead of swapping for boxes. Use `animate-pulse` + `opacity` on the actual DOM while keeping exact dimensions and multi-line structure preserved.

## Patterns

### Per-Line Quantity Skeleton
**❌ Old approach (layout shift):**
```tsx
{isPending ? (
<div className="skeleton h-9 w-24" />
) : (
<QuantitySelector ... />
)}
```

**✅ New approach (no shift):**
```tsx
<QuantitySelector
disabled={loading || isGift || isPending} // stay disabled, not hidden
quantity={quantity}
...
/>
```
The selector is always visible and always takes up the same space. While pending, it's just disabled (the user can't interact, but the visual layout is stable).

### Price Block with Pulse
**❌ Old approach (layout shift):**
```tsx
{isPending ? (
<div className="skeleton h-5 w-16" />
) : (
<>
{sale != list && (
<span className="text-[#AAA89C] text-xs line-through">
{formatPrice(list, currency, locale)}
</span>
)}
<span className="text-base font-semibold">
{formatPrice(sale, currency, locale)}
</span>
</>
)}
```

**✅ New approach (no shift):**
```tsx
<div className={`flex flex-col justify-end items-end ${isPending ? 'animate-pulse opacity-40' : ''}`}>
{sale != list && (
<span className="text-[#AAA89C] text-xs line-through">
{formatPrice(list, currency, locale)}
</span>
)}
<span className="text-base font-semibold">
{formatPrice(sale, currency, locale)}
</span>
</div>
```
The block stays in the DOM with its 2 lines intact. When pending, it pulses (opacity drop + animation). Exact dimensions are preserved.
Comment on lines +58 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reserve the discount row when no sale price exists.

{sale != list && (...)} still changes the block from two lines to one when the discount disappears, so this example does not guarantee zero layout shift. Reserve a fixed two-line slot with an invisible placeholder or matching min-height.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/deco-micro-skeletons.md` around lines 58 - 69, Update the price block
containing the sale and list prices so it always reserves space for two lines
when sale equals list or no discount exists. Keep the discount text visually
hidden or apply an equivalent matching min-height, while preserving the existing
visible strikethrough behavior when sale differs from list and the pending
animation.


### Cart Footer Total
Same pattern:
```tsx
<span
className={`text-lg font-semibold transition-opacity ${isMutating ? 'animate-pulse opacity-40' : ''}`}
>
{formatPrice(total, currency, locale)}
</span>
```

## Benefits
- **Zero layout shift:** No CLS violation, no row collapse.
- **Visual feedback:** User still sees loading state (pulse + opacity).
- **No interaction layer:** No need to disable the real widget — it's just dimmed.
- **Semantic:** The actual content stays in the DOM; CSS handles the visual feedback.
Comment on lines +81 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve the disabled/clickable contradiction.

The example sets disabled={...}, but the benefits say the widget need not be disabled and verification says it stays clickable. A disabled control is not clickable; describe this as “stays visible but disabled,” and document whether pending input is rejected or queued.

Also applies to: 96-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/deco-micro-skeletons.md` around lines 81 - 85, Update the
loading-state documentation and verification text to resolve the contradiction
around disabled controls: describe the widget as remaining visible but disabled,
and explicitly document whether input received while pending is rejected or
queued. Apply the same wording consistently in the Benefits section and the
corresponding lines around the example.


## Trade-offs
- The pulse effect is subtle — ensure it's visible enough (opacity 0.4–0.5 works).
- Not suitable for skeleton screens that show "shape hints" (e.g., placeholder text lines) — those need fixed boxes. Use micro-skeletons only for fields that recalculate, not for structure that changes.

## CSS Classes Used
- `animate-pulse` — DaisyUI / Tailwind built-in (oscillates opacity 0.5 ↔ 1).
- `opacity-40` — dims the content while pulsing.
- `transition-opacity` — smooths the opacity change.

## Verification
- **Visual:** Open cart, change quantity. The line doesn't collapse; price pulses in place.
- **CLS:** Lighthouse score doesn't drop due to layout shift.
- **Interaction:** Quantity selector stays clickable (disabled state prevents actual mutation, but the DOM is stable).
123 changes: 123 additions & 0 deletions skills/deco-minicart-configuravel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: Configurable On-Demand Minicart (TanStack / React Query)
description: Build an API-frugal, CMS-configurable VTEX minicart for Deco storefronts on TanStack Start with React Query. No getOrCreateCart on page load, lazy orderForm creation, canonical Minicart shape, micro-skeletons, and toast-vs-drawer toggle.
reference: montecarlo-tanstack
tags: [minicart, react-query, vtex, performance, cms, ux]
---

# Configurable On-Demand Minicart (TanStack / React Query / VTEX)

Turn a VTEX minicart into an API-frugal, CMS-configurable, replicable component on `@decocms/start` (TanStack Start / React / Cloudflare) with `@decocms/apps-vtex@7.20+`.

**Reference implementation:** Monte Carlo (`montecarlo-tanstack`).

## Goals this Delivers

1. **Zero `getOrCreateCart` calls on page load / F5.** The cart is a react-query query gated so a returning shopper reloading a page triggers ZERO orderForm calls. Header badge renders from a lightweight `cart_item_count` cookie instead.
2. **Empty cart without API calls.** A cookieless visitor opening the drawer sees an empty state with zero API calls; the orderForm is created only on the first add-to-cart.
3. **Canonical `Minicart` shape.** Adopt the platform-agnostic `Minicart` type from `@decocms/apps-vtex/utils/minicart` so totals, currency, locale, and free-shipping math come from one boundary conversion, not ad-hoc field digging.
4. **Micro-skeletons without layout shift.** Per-line quantity/price skeletons + footer total skeleton via pulse-in-place (not fixed boxes), preserving exact dimensions and preventing row collapse.
5. **CMS-editable config + composable shelf.** A loader-based config with live Preview (Farm pattern) + a slot for dropping product shelves inside the cart via `SectionRenderer`.
6. **Toast-vs-drawer toggle.** A "notification (toast)" switch: ON (default) = toast on add + drawer stays closed; OFF = drawer auto-opens. Driven by CMS config.

## Architecture

### On-Demand React Query Cart

`src/sdk/cart/` holds the core SDK:

- **`queries.ts`** — `cartKeys`, orderForm cookie helpers, the `cart_item_count` badge cookie, and `shouldFetchCart(displayCartIntent)` (the fetch gate).
- **`useCartQuery.ts`** — `useCart()`: react-query query + optimistic mutations. Exposes legacy signal-shaped surface (`cart.value`, `loading.value`) AND the canonical `minicart` via `useMemo`.
- **`config.ts`** — Module-level `cartConfig` signal + `setCartConfig` (toast, free-shipping threshold, coupon toggle, checkout href). Read by add-button and toast island without prop drilling.

**The fetch gate** is the heart of goal #1/#2:
```ts
// shouldFetchCart(intent) — fetch ONLY when the shopper intends to open AND a cart exists
// (a persisted orderFormId cookie OR a mutation ran this session).
return displayCartIntent && (Boolean(readOrderFormCookie()) || _mutationRanThisSession);
```

Badge count is served from the `cart_item_count` cookie (written on every commit + optimistic patch), read client-only in a `useEffect` to avoid SSR hydration mismatch.

### Invoke vs Direct Fetch (Critical Reconciliation)

`@decocms/apps-vtex@7.20`'s stock `hooks/useCart` does browser `fetch("/api/checkout/pub/orderForm")` — only works on a VTEX-proxied domain. Deco storefronts on custom domains do NOT proxy `/api/checkout`; they use the `invoke` server-function proxy.

**Do NOT adopt the stock hook as-is.** Instead **graft** the pure/portable parts:

- The canonical type `Minicart` (`@decocms/apps-commerce/types`)
- The transform `vtexOrderFormToMinicart` (`@decocms/apps-vtex/utils/minicart`)
- The `loaders/minicart` "empty shell when no cookie" pattern

onto your existing `invoke`-based, on-demand local cart. Compute `minicart` with `useMemo`:

```ts
const minicart = useMemo(() => data ? vtexOrderFormToMinicart(data, {
freeShippingTarget: config.freeShippingTarget,
checkoutHref: config.checkoutHref,
enableCoupon: config.enableCoupon,
}) : null, [data, config.freeShippingTarget, config.checkoutHref, config.enableCoupon]);
```

**Alternative (out of scope):** Reverse-proxy `/api/checkout` → VTEX at the edge to use the stock hook directly. Document, don't implement.

### CMS Config as Loader with Preview (Not a Section)

The minicart drawer is a **layout-shell overlay** (always mounted in `Header/Drawers`, opened by a global `displayCart` signal). It is NOT a page section — don't try to make it one.

- **`src/loaders/minicart.tsx`** — Identity loader returning `MinicartConfig` (rich JSDoc: `freeShippingTarget`, `enableCoupon`, `checkoutHref`, `variant`, `showAddToCartToast`, `addedToast`, `emptyState`, `shelfSections?: Section[]`). Also `export const Preview = (config) => JSX` — a self-contained HTML preview of the configured minicart open and populated (Farm pattern, e.g. `deco-sites/farmrio/loaders/Layouts/Tags.tsx`). Keep Preview dependency-free (no runtime hooks).
- **Header receives flat config object** (`cart.config?: MinicartConfig`), passes it through to `Drawers → Cart`. No `SectionRenderer` wrapping the drawer.
- **Composable shelf slot** — `common/Cart.tsx` renders `shelfSections` via `SectionRenderer` so the admin can drop a product shelf (Granado style) inside the cart. Scope is localized.

## File Map (Copy/Adapt per Site)

| File | Role |
|---|---|
| `src/sdk/cart/queries.ts` | Fetch gate, orderForm + `cart_item_count` cookies |
| `src/sdk/cart/useCartQuery.ts` | `useCart()`, react-query, optimistic mutations, `minicart` graft |
| `src/sdk/cart/config.ts` | `cartConfig` signal + `setCartConfig` |
| `src/loaders/minicart.tsx` | `MinicartConfig` + identity loader + `Preview` |
| `src/components/miniCart/common/Cart.tsx` | Drawer body, empty state, shelf slot, micro-skeletons |
| `src/components/miniCart/vtex/Cart.tsx` | Adapter: `minicart.storefront` → BaseCart props |
| `src/components/miniCart/AddedToCartToast.tsx` | Toast island (photo, price, type, message) |
| `src/components/Header/Drawers.tsx` | Hosts drawer + toast; **subscribes display signals via `useSignalValue`** |
| `src/components/Header/Header.tsx` | Publishes config via `setCartConfig`, passes to Drawers |
| `src/components/Header/Buttons/Cart/{common,vtex}.tsx` | Badge from cookie + hover prefetch |
| `src/components/Product/AddToCartButton/{common,vtex}.tsx` | Optimistic toast/drawer + real product `image` prop |

## Gotchas (These Cost the Most Time)

### 1. Signal Reactivity (Preact → React)
Reading `signal.value` directly in render does NOT re-render a React component (unlike @preact/signals).

**Symptom:** Drawer "does not open" — the click fires (analytics logs) and sets `displayCart.value=true`, but nothing re-renders.

**FIX:** Subscribe with `useSignalValue(sig)` (useSyncExternalStore) for every render-time read of a module signal (`displayCart`, `cartConfig`, `cartToast`). Writes in handlers stay `sig.value = x`.

### 2. Optimistic Toast Timing
Fire the toast / open the drawer BEFORE `await onAddItem()`, not after — otherwise feedback is delayed by the server round-trip and never shows if the mutation rejects. The mutation carries its own optimistic patch + rollback.

### 3. Toast Photo
`mapProductToAnalyticsItem` gives `item_url` (product page URL), NOT an image. Thread the real image (`product.image?.[0]?.url`) into the add button and use it for the toast.

### 4. Directory Casing (macOS vs Linux CI)
The git index may hold `minicart`/`ui` lowercase while the macOS working tree shows `miniCart`/`UI`. Import using git-indexed casing (`~/components/minicart/...`) or Linux CI + `tsc` (TS1149/TS1261) breaks. Check with `git ls-files | grep -i <path>`.

### 5. CMS Codegen Migration Blocker (7.20+ Bump)
After bumping to `@decocms/*@7.20+`, the generators (`@decocms/blocks-cli`) write to `.deco/` in a NEW format, but a repo migrated earlier still consumes OLD-format files in `src/server/{cms,admin}/` (from `@decocms/start`). Running the generators does NOT update what the app reads.

**Consequence:** New CMS props (`cart.config`, toast toggle) are code-ready but NOT admin-editable until the repo does the codegen migration (switch `setup.ts` importers to `.deco/` OR regenerate all artifacts consistently).

**Mitigation:** Design runtime defaults so the site behaves correctly WITHOUT any CMS config (e.g. `autoOpenOnAdd: false` → toast active, `freeShippingTarget: 500`). Then editability lands for free once the migration runs.
Comment on lines +106 to +111

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the documented configuration key for the runtime default.

MinicartConfig documents showAddToCartToast, but the migration mitigation introduces autoOpenOnAdd. This is a contract mismatch: implementers may configure a key that the loader and header never read.

Proposed correction
-Then editability lands for free once the migration runs.
+Then editability lands for free once the migration runs.

Use the existing documented field in the preceding example, e.g. showAddToCartToast: true, or explicitly document a separate autoOpenOnAdd field and its mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/deco-minicart-configuravel.md` around lines 106 - 111, Update the
runtime-default mitigation in the CMS Codegen Migration Blocker section to use
the documented MinicartConfig key showAddToCartToast instead of autoOpenOnAdd.
Keep the intended default behavior of enabling the add-to-cart toast, unless you
explicitly document autoOpenOnAdd and its mapping to showAddToCartToast.


## Verification Checklist

- **Types:** `npx tsc --noEmit` — compare error COUNT to a pre-change baseline. Zero NEW errors is the bar.
- **Browser (with dev server):**
- F5 with an existing cart cookie → **no** `orderForm` POST.
- Drawer opens cookieless → empty state, **zero** API calls.
- Add-to-cart → orderForm created once; with toast ON → toast (photo/price/type), drawer stays closed; with toast OFF → drawer opens.
- Change quantity → skeleton only on that line + total, rest stable (no layout shift).
- Hover on icon with cookie → prefetch cart before click.
- **SSR check:** `curl -s localhost:PORT/ | grep data-qa-minicart` returns nothing (drawer body must not be in SSR HTML).
- **Admin (post-codegen-migration):** Edit Minicart config (free-shipping threshold, coupon toggle, toast label), see shelf composability work, Preview reflects changes.
Loading
Loading