-
Notifications
You must be signed in to change notification settings - Fork 55
docs: add reusable Deco skills documentation #1215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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. | ||
|
|
||
| ## 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' . || trueRepository: 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 🤖 Prompt for AI Agents |
||
| .inputValidator((data: { | ||
| orderFormId: string; | ||
| orderItems: Array<{ id: string; seller: string; quantity: number }>; | ||
| }) => data) | ||
|
Comment on lines
+30
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 || trueRepository: 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
doneRepository: deco-cx/deco Length of output: 1458 🌐 Web query:
💡 Result: In TanStack Start, the Citations:
Validate the add-to-cart payload at the server boundary. 🤖 Prompt for AI Agents |
||
| .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. | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
🤖 Prompt for AI Agents |
||
|
|
||
| ### 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Also applies to: 96-99 🤖 Prompt for AI Agents |
||
|
|
||
| ## 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). | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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. 🤖 Prompt for AI Agents |
||
|
|
||
| ## 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. | ||
There was a problem hiding this comment.
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-L76skills/deco-add-to-cart-slim.md#L84-L85skills/README.md#L12-L15🤖 Prompt for AI Agents