@wix/agent-skills 1.19.0 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +33 -31
  3. package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
  4. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +17 -8
  5. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
  6. package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
  7. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +88 -73
  8. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +46 -22
  9. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +20 -6
  10. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
  11. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +71 -31
  12. package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
  13. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +4 -5
  14. package/skills/wix-app/scripts/pkg-root.cjs +73 -0
  15. package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
  16. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
  17. package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
  18. package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
  19. package/skills/wix-base44-connector/SKILL.md +67 -43
  20. package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
  21. package/skills/wix-docs/SKILL.md +47 -20
  22. package/skills/wix-headless/SKILL.md +1 -0
  23. package/skills/wix-headless/entry/bootstrap.mjs +106 -50
  24. package/skills/wix-headless/entry/skill.md +5 -3
  25. package/skills/wix-headless/references/CAPABILITIES.md +7 -2
  26. package/skills/wix-headless/references/SDK_HANDOFF.md +2 -1
  27. package/skills/wix-headless/references/inline-recipes/experience-store.md +48 -0
  28. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +350 -114
  29. package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +1 -1
  30. package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
  31. package/skills/wix-headless/references/managed/CREATE.md +5 -0
  32. package/skills/wix-headless-fast/SKILL.md +25 -7
  33. package/skills/wix-headless-fast/entry/skill.md +23 -14
  34. package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
  35. package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
  36. package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
  37. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
  38. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
  39. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
  40. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
  41. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
  42. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
  43. package/skills/wix-manage/SKILL.md +16 -10
  44. package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +4 -1
  45. package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -1
  46. package/skills/wix-manage/references/bookings/create-class-service.md +3 -1
  47. package/skills/wix-manage/references/bookings/create-course-service.md +4 -2
  48. package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +68 -3
  49. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-seasonal-promotion.md +1 -1
  50. package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
  51. package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
  52. package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
  53. package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
  54. package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
  55. package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +108 -14
  56. package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
  57. package/skills/wix-manage/references/seo/manage-content-plan.md +182 -136
  58. package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
  59. package/skills/wix-manage/references/sites/upload-static-site.md +283 -0
  60. package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
  61. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +3 -1
  62. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +3 -1
  63. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  64. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +69 -27
  65. package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +32 -14
  66. package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +14 -3
  67. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +41 -35
  68. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +77 -30
  69. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +31 -2
  70. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +31 -6
  71. package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
  72. package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
  73. package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
  74. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
  75. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
  76. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
  77. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
  78. package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
  79. package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
  80. package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
@@ -1,30 +1,31 @@
1
1
  # Storefront — playbook
2
2
 
3
3
  The commerce machinery ships as files — data layer, hooks, cart, checkout, SEO plumbing,
4
- typed end-to-end. **The presentation is yours**: you design and implement the product card,
5
- the grid, and the PDP surface on the shipped hooks/DTOs, plus the home page and the brand.
6
- You never write commerce code; you never skip designing the store.
4
+ typed end-to-end. **The presentation doesn't ship — you build it** on the shipped hooks/DTOs:
5
+ the shop and product (PDP) pages with their islands, the home page, and the brand. The
6
+ skeletons below carry each page's contract — including the SSR and SEO machinery, which must
7
+ be exact. You never write commerce code; you never skip designing the store.
7
8
 
8
9
  ## The file map (deployed into `src/`)
9
10
 
10
11
  **Don't read the shipped files** — this table and the contracts below are everything you
11
- need. Open a shipped file's source **only** on a real fallback: a runtime error, a field this
12
- playbook doesn't cover, or when a *reference* component's pattern is explicitly worth reading
13
- (marked below). Files you edit: `SiteLayout.astro`, `styles/global.css`, and the two page
14
- imports you swap to your own components.
12
+ need. Open a shipped file's source **only** on a real fallback: a runtime error, or a field
13
+ this playbook doesn't cover. Files you edit: `SiteLayout.astro` and `styles/global.css`.
14
+ Files you **create** (skeletons below): the shop and PDP pages with their island components,
15
+ plus your home page.
15
16
 
16
17
  | file | what it is |
17
18
  |---|---|
18
19
  | `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
19
20
  | `wix/media.ts` · `wix/money.ts` | `imgSrc()` / `formatMoney()` — already used by everything shipped |
20
21
  | `wix/storefront/types.ts` | the DTOs (`ProductSummary`, `ProductDetail`, `Cart`, `Category`) — contracts inlined below |
21
- | `wix/storefront/catalog.ts` | `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `resolveVariant` |
22
+ | `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/search + cursor paging, all server-side), `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `resolveVariant` |
22
23
  | `wix/storefront/cart.ts` · `cart-store.ts` | Cart V2 + shared cart state (module store — spans Astro islands) |
23
24
  | `hooks/storefront/useCart.ts` | cart state + actions — contract below |
24
25
  | `hooks/storefront/useShop.ts` | listing + live category filter — contract below |
25
26
  | `hooks/storefront/useProductDetail.ts` | option selection → variant resolution → add-to-cart — contract below |
26
27
  | `components/storefront/CartButton.tsx` · `CartDrawer.tsx` | header badge + slide-over cart — **wire as-is** (drawer once per page) |
27
- | `components/storefront/ShopView.tsx` · `ProductCard.tsx` · `ProductGrid.tsx` · `ProductDetailView.tsx` · `VariantPicker.tsx` | **REFERENCE implementations** — correct, plain, deliberately generic. Read for patterns if useful; **build your own instead of shipping them** (below) |
28
+ | `components/storefront/ShopView.tsx` · `ProductDetailView.tsx` | **don't ship — YOU create them** (skeletons below): the client islands your shop and PDP pages mount |
28
29
  | `styles/global.css` | **the design system**: Tailwind v4 + the `@theme` token block (colors, radii, fonts — same token family as the official Wix templates). Everything, shipped and yours, styles from these tokens |
29
30
 
30
31
  Astro stack additionally gets:
@@ -32,24 +33,26 @@ Astro stack additionally gets:
32
33
  | file | what it is |
33
34
  |---|---|
34
35
  | `layouts/SiteLayout.astro` | the site chrome — **yours to brand**: header, footer, nav. Keep the `<slot name="seo-tags" />`, the global.css import, and the CartButton/CartDrawer mounts |
35
- | `pages/shop.astro` | SSR listing page — **keep its frontmatter** (the data fetching), swap the island import to YOUR shop component |
36
- | `pages/products/[slug].astro` | SSR PDP with owner-editable SEO — **keep its frontmatter and the SEO pieces** (`wixMetadata`, `loadSEOTagsServiceConfig`, `<SEO.Tags>`) exactly, swap the island import to YOUR detail component |
36
+ | `pages/shop.astro` · `pages/products/[slug].astro` | **don't ship — YOU create them** (skeletons below): thin SSR pages that fetch server-side and mount your islands; the PDP page carries the owner-editable SEO machinery, exactly as its skeleton shows |
37
37
 
38
38
  ## What you build — this is the design job, not optional polish
39
39
 
40
- You implement **four surfaces yourself**, styled with Tailwind utilities on the `@theme`
41
- tokens, designed to fit the brief (the business, the tone, the audience — a toy brand and a
42
- jewelry house should not get the same store):
40
+ You implement **three surfaces yourself** — each an SSR page plus its island, per the
41
+ skeletons below — styled with Tailwind utilities on the `@theme` tokens, designed to fit the
42
+ brief (the business, the tone, the audience — a toy brand and a jewelry house should not get
43
+ the same store):
43
44
 
44
- 1. **The product card + grid** — your tile design (image treatment, badges, price/sale
45
- presentation, hover behavior) and your grid rhythm (columns, density, maybe an editorial
46
- featured tile). Keep skeleton-while-loading and an honest empty state (the reference
47
- `ProductGrid` shows the pattern — `products === null` → skeletons, `[]` → empty message).
48
- 2. **The shop surface** — category filter + your grid, on `useShop`.
49
- 3. **The PDP surface** — gallery, price/sale, description, your option-selection UI (color
50
- options = real swatches), quantity, add-to-cart — on `useProductDetail`, which owns ALL
51
- selection/variant logic; you own how it looks.
52
- 4. **The home page** — hero, featured products (fetch in frontmatter → your grid), brand story.
45
+ 1. **The shop page + `ShopView`** (skeletons below) — category filter + your grid of your
46
+ tiles: image treatment, badges, price/sale presentation, hover behavior, grid rhythm
47
+ (columns, density, maybe an editorial featured tile). `products === null` → skeleton tiles,
48
+ `[]` → an honest empty state. Decompose into `ProductCard`/`ProductGrid` files if you like —
49
+ your call, nothing prescribes it.
50
+ 2. **The PDP page + `ProductDetailView`** (skeletons below) — gallery, price/sale,
51
+ description, your option-selection UI (color options = real swatches), quantity,
52
+ add-to-cart — on `useProductDetail`, which owns ALL selection/variant logic; you own how it
53
+ looks. This is the surface that most often looks generic — make the layout the brand's: an
54
+ editorial split, a sticky buy column, a full-bleed gallery.
55
+ 3. **The home page** — hero, featured products (fetch in frontmatter → your grid), brand story.
53
56
 
54
57
  Plus the **theme** (edit the `@theme` block in `styles/global.css` — one edit; a dark brand is
55
58
  flipped token values; add brand fonts as extra tokens) and the **chrome** (header/footer in
@@ -64,10 +67,17 @@ flipped token values; add brand fonts as extra tokens) and the **chrome** (heade
64
67
  // imageUrl, hoverImageUrl, optionsSummary /* "2 colors · 3 sizes" */, quickAddable: boolean }
65
68
  // price vs maxPrice differ → show a range. quickAddable → useCart().addToCart(product.id).
66
69
 
67
- // useShop({ initialProducts?, initialCategories? }) →
70
+ // useShop({ initialProducts?, initialCategories?, pageSize? /* 24 */ }) →
68
71
  // { products: ProductSummary[]|null /* null = loading → skeletons */, categories: Category[],
69
- // activeCategoryId: string|null, setActiveCategoryId(id|null), loading, error }
72
+ // activeCategoryId: string|null, setActiveCategoryId(id|null),
73
+ // sort: keyof SORTS, setSort(sort), filters, setFilters({ minPrice?, maxPrice?, inStockOnly?, search? }),
74
+ // loading, error, retry(), hasMore, loadMore(), loadingMore }
70
75
  // Category = { id, slug, name }. Render the filter bar only when categories.length > 1.
76
+ // Sort/filter/search/paging run on Wix across the WHOLE catalog (a change restarts the list).
77
+ // SORTS (exported next to useShop) is Record<sortKey, { label: string }> — the value is an
78
+ // OBJECT, so render entry.label, never the entry itself:
79
+ // Object.entries(SORTS).map(([key, { label }]) => <option value={key}>{label}</option>)
80
+ // hasMore → render a "load more" control calling loadMore() (disabled while loadingMore).
71
81
 
72
82
  // useProductDetail({ initial? /* SSR */, slug? /* SPA */ }) →
73
83
  // { product: ProductDetail|null, notFound,
@@ -84,16 +94,165 @@ flipped token values; add brand fonts as extra tokens) and the **chrome** (heade
84
94
  // { cart: { lines, itemCount, subtotal, currency }|null, busy, error, open,
85
95
  // addToCart(productId, variantId?, qty?, extras?), updateQuantity(lineItemId, qty),
86
96
  // removeLine(lineItemId), checkout(), openCart(), closeCart(), refresh() }
97
+ // addToCart rejects on refusal (out of stock, digital product with no file) AND records
98
+ // .error, opening the drawer either way — so render .error in whatever surface you build for
99
+ // the cart, and never chain checkout() onto an add without awaiting it successfully.
100
+ ```
101
+
102
+ ### The pages and islands you create — skeletons
103
+
104
+ Nothing renders until you write these — the store IS your work. Each page is a thin SSR shell
105
+ (fetch → DTO props → island); each island is a thin view over a hook. The pages' frontmatter
106
+ is **machinery, not design** — reproduce it as the skeletons show, exactly. Hooks first,
107
+ branches after (an early return above a hook changes hook order between renders and React
108
+ throws). The islands render on the server too (`client:load` SSRs), and by then the 200 and
109
+ headers are already sent — a render throw truncates the body mid-stream and surfaces to the
110
+ visitor as `ERR_HTTP2_PROTOCOL_ERROR`, not an error page. Render every state totally; nothing
111
+ in a render path may throw.
112
+
113
+ ```astro
114
+ ---
115
+ // src/pages/shop.astro — YOU create it. Products and categories are fetched SERVER-SIDE
116
+ // (SEO: view-source shows product names) and handed to the island as serialized DTO props;
117
+ // category switches then filter live on the client.
118
+ import SiteLayout from "../layouts/SiteLayout.astro";
119
+ import ShopView from "../components/storefront/ShopView";
120
+ import { fetchProducts, fetchCategories } from "../wix/storefront/catalog";
121
+ import type { Category, ProductSummary } from "../wix/storefront/types";
122
+
123
+ let products: ProductSummary[] = [];
124
+ let categories: Category[] = [];
125
+ try {
126
+ [products, categories] = await Promise.all([fetchProducts({ limit: 24 }), fetchCategories()]);
127
+ } catch {
128
+ // Guarded: an unhandled SSR throw truncates the response mid-stream; the island
129
+ // renders your empty state instead.
130
+ }
131
+ ---
132
+ <SiteLayout title="Shop">
133
+ <!-- your page heading / intro, then: -->
134
+ <ShopView client:load initialProducts={products} initialCategories={categories} />
135
+ </SiteLayout>
136
+ ```
137
+
138
+ ```astro
139
+ ---
140
+ // src/pages/products/[slug].astro — YOU create it. This is a Wix ITEM PAGE: the wixMetadata
141
+ // export + <SEO.Tags> are what let the site owner edit this page's title/description/OG in
142
+ // the dashboard and register the route in the sitemap. All three SEO pieces are REQUIRED,
143
+ // exactly as here.
144
+ import SiteLayout from "../../layouts/SiteLayout.astro";
145
+ import ProductDetailView from "../../components/storefront/ProductDetailView";
146
+ import { fetchProductBySlug } from "../../wix/storefront/catalog";
147
+ import { WIX_APPS } from "@wix/essentials";
148
+ import { SEO } from "@wix/seo/components";
149
+ import { loadSEOTagsServiceConfig } from "@wix/seo/services";
150
+ import { seoTags } from "@wix/seo";
151
+
152
+ export const wixMetadata = {
153
+ appDefId: WIX_APPS.checkoutAndOrders.id,
154
+ pageIdentifier: WIX_APPS.checkoutAndOrders.productPageMetadata.pageIdentifier,
155
+ identifiers: { slug: WIX_APPS.checkoutAndOrders.productPageMetadata.identifiers.handle },
156
+ };
157
+
158
+ const slug = Astro.params.slug!;
159
+
160
+ // Behind Wix's proxy the request URL is the internal one — the real public page URL arrives
161
+ // on x-wix-forwarded-url, and the SEO service needs the public one.
162
+ const forwardedUrl = Astro.request.headers.get("x-wix-forwarded-url");
163
+ const pageUrl =
164
+ forwardedUrl && URL.canParse(forwardedUrl) && /^https?:$/.test(new URL(forwardedUrl).protocol)
165
+ ? forwardedUrl
166
+ : Astro.url.href;
167
+
168
+ let product = null;
169
+ let seoTagsServiceConfig = null;
170
+ try {
171
+ [product, seoTagsServiceConfig] = await Promise.all([
172
+ fetchProductBySlug(slug),
173
+ loadSEOTagsServiceConfig({
174
+ pageUrl,
175
+ itemType: seoTags.ItemType.STORES_PRODUCT,
176
+ itemData: { slug },
177
+ }),
178
+ ]);
179
+ } catch {
180
+ // Guarded: an unhandled SSR throw truncates the response mid-stream.
181
+ }
182
+
183
+ if (!product) {
184
+ return new Response(null, { status: 404 });
185
+ }
186
+ ---
187
+ <SiteLayout title={product.name}>
188
+ <SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
189
+ <ProductDetailView client:load initial={product} />
190
+ </SiteLayout>
191
+ ```
192
+
193
+ ```tsx
194
+ // src/components/storefront/ShopView.tsx — YOU build it; your shop.astro mounts it.
195
+ import { useShop } from "../../hooks/storefront/useShop";
196
+ import type { Category, ProductSummary } from "../../wix/storefront/types";
197
+
198
+ export default function ShopView(props: {
199
+ initialProducts?: ProductSummary[]; // SSR props from your shop.astro — pass straight
200
+ initialCategories?: Category[]; // to useShop; omitted in a SPA (client fetch)
201
+ }) {
202
+ const { products, categories, activeCategoryId, setActiveCategoryId, loading, error } =
203
+ useShop(props);
204
+ // …you implement the render:
205
+ // • category pills when categories.length > 1 — All + one per category,
206
+ // driving setActiveCategoryId(id | null)
207
+ // • a sort control over SORTS driving setSort; filters (price bounds, in-stock,
208
+ // name search) via setFilters — include the ones that fit the brief, not all of them
209
+ // • error → a short inline message (retry() re-runs the query)
210
+ // • products === null (or loading) → skeleton tiles; [] → your honest empty state
211
+ // • else YOUR grid of YOUR tiles (ProductSummary contract above): image
212
+ // (hoverImageUrl on hover), name, price/compareAtPrice, ribbon, optionsSummary;
213
+ // tile links to `/products/${p.slug}`; quickAddable → useCart().addToCart(p.id)
214
+ // • hasMore → your "load more" control calling loadMore() (disabled while loadingMore)
215
+ }
216
+ ```
217
+
218
+ ```tsx
219
+ // src/components/storefront/ProductDetailView.tsx — YOU build the whole PDP surface;
220
+ // your [slug].astro mounts it with the server-fetched product.
221
+ import { useProductDetail } from "../../hooks/storefront/useProductDetail";
222
+ import type { ProductDetail } from "../../wix/storefront/types";
223
+
224
+ export default function ProductDetailView(props: {
225
+ initial?: ProductDetail | null; // SSR (Astro); a SPA passes { slug } instead
226
+ slug?: string;
227
+ }) {
228
+ const d = useProductDetail(props); // full contract above — selection lives HERE
229
+ // …you implement the render. Handle in order:
230
+ // d.notFound → a "doesn't exist (anymore)" message
231
+ // !d.product → a loading placeholder
232
+ // else the product view, laid out for the brand:
233
+ // • gallery from d.product.gallery (urls, main first): show ONE primary
234
+ // image at full size, and the rest as a small thumbnail strip (or a
235
+ // scrollable row) that swaps the primary — never map the whole gallery
236
+ // to full-width images stacked down the column (products with per-color
237
+ // linked media carry several gallery urls, so that stacks big duplicates).
238
+ // A single-image gallery is just the one primary — no empty strip.
239
+ // • name, live d.price / d.compareAtPrice (strike only when they differ),
240
+ // descriptionHtml rendered as HTML
241
+ // • option controls from d.optionGroups → d.selectOption(optionName, choiceName)
242
+ // (isColor → real swatches via colorCode; disable out-of-stock choices);
243
+ // modifiers from d.product.modifiers → d.setModifier(key, value), "*" = mandatory
244
+ // • quantity (d.quantity / d.setQuantity), then the buy button gated by d.canAdd
245
+ // ONLY — never resolve variants yourself — calling d.add(); d.adding disables,
246
+ // d.error renders inline
247
+ }
87
248
  ```
88
249
 
89
250
  ### Wiring — Astro (default)
90
251
 
91
252
  1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass).
92
- 2. Write your components under `src/components/storefront/` (new file names — don't overwrite
93
- the references), then **swap the island import** in `pages/shop.astro` and
94
- `pages/products/[slug].astro` to yours — keep each page's frontmatter (data fetching, SEO
95
- pieces) exactly as shipped. Primary-content islands mount `client:load` with the SSR props;
96
- browser-state widgets (cart) are `client:only="react"` — the shipped mounts show this.
253
+ 2. Create the shop and PDP pages and their islands per the skeletons above — frontmatter
254
+ machinery exact, presentation yours. Primary-content islands mount `client:load` with the
255
+ SSR props; browser-state widgets (cart) are `client:only="react"`.
97
256
  3. Write `pages/index.astro` (home) on `SiteLayout`.
98
257
 
99
258
  ### Wiring — React SPA (Vite etc.)
@@ -109,7 +268,7 @@ client id into `wix/config.ts`; nothing else to configure.
109
268
 
110
269
  - **Data and commerce logic only through the shipped exports** — never rewrite their
111
270
  internals or re-derive a request shape. Extend by calling the exports or adding a new
112
- function in `wix/storefront/` for a genuine gap (API contracts: the `wix-docs` skill).
271
+ function in `wix/storefront/` for what they don't cover (API contracts: the `wix-docs` skill).
113
272
  - **Selection→cart goes through `useProductDetail`** — never add a product with options by
114
273
  picking `variants[0]`, and never gate `canAdd` yourself.
115
274
  - Don't wrap shipped calls in your own API routes — they run client-side by design.
@@ -117,8 +276,18 @@ client id into `wix/config.ts`; nothing else to configure.
117
276
  parallel theme files, no hardcoded palette values in components.
118
277
  - Checkout only through the shipped cart (`checkout()`) — never a hand-built checkout URL.
119
278
  - Live data or an honest empty state — never mock products, prices, reviews, or counts.
120
- - Keep the PDP page's SEO pieces (`wixMetadata` + `loadSEOTagsServiceConfig` + `<SEO.Tags>`)
121
- exactly as shipped — owners edit those tags in their dashboard.
279
+ - Your PDP page carries the SEO pieces (`wixMetadata` + `loadSEOTagsServiceConfig` +
280
+ `<SEO.Tags>`) exactly as the skeleton shows — owners edit those tags in their dashboard.
281
+ - **Browsing, cart and checkout need no login.** They run on the Wix visitor session the
282
+ shipped SDK client already holds. Don't gate the shop, the PDP or the cart behind sign-in,
283
+ and don't add a members/auth flow unless the brief actually asks for accounts.
284
+ - **Call every hook before any conditional return.** A PDP that returns early for
285
+ `notFound`/loading above its `useState`/`useEffect` changes hook order between renders and
286
+ React throws. Hooks first, branches after.
287
+ - **Sort and filter at the source, not on a loaded page.** `useShop`'s sort/filters/search and
288
+ `searchCatalog` run on Wix across the whole catalog before cursor paging; re-ordering the
289
+ array a hook already returned only sorts the slice you happen to have. Never sort/filter
290
+ client-side, and never raise the page limit instead of paging (`hasMore`/`loadMore`).
122
291
 
123
292
  ## Point the user to their dashboard
124
293
 
@@ -144,6 +313,6 @@ a color option, ≥1 on sale, an image per product) unless the brief says otherw
144
313
  a reload (same visitor token).
145
314
  - [ ] Checkout button redirects to Wix-hosted checkout.
146
315
  - [ ] PDP view-source carries the SEO tags (Astro).
147
- - [ ] Card/grid/PDP/home are YOUR designs on the tokens — not the shipped references; the
148
- data-layer/hook/cart files are unedited.
316
+ - [ ] Shop/PDP/home are YOUR designs on the tokens; the data-layer/hook/cart files are
317
+ unedited.
149
318
  - [ ] Dashboard links handed to the owner.
@@ -1,21 +1,33 @@
1
- // Catalog listing + category filter for a shop/listing surface.
1
+ // Catalog listing for a shop surface: category filter, sort, price/stock/search filters, and
2
+ // cursor paging — all applied by Wix across the whole catalog (searchCatalog), never to a page
3
+ // already in hand. A changed selection starts a fresh cursor chain.
2
4
  //
3
- // SSR-friendly: when the host fetched the data already (Astro frontmatter, a server
4
- // component), pass it as `initial` and no client fetch happens for it. In a pure SPA,
5
- // pass nothing and the hook fetches on mount. Category switches always fetch live
6
- // (server-side-filtered by the category id).
7
- import { useEffect, useState } from "react";
5
+ // SSR-friendly: pass `initialProducts`/`initialCategories` (Astro frontmatter) and they render
6
+ // immediately — the hook still revalidates the first page once on mount to open the cursor
7
+ // chain, replacing the seed without a skeleton flash.
8
+ import { useCallback, useEffect, useRef, useState } from "react";
8
9
  import {
10
+ CATALOG_SORTS,
9
11
  fetchCategories,
10
- fetchProducts,
11
- fetchProductsByCategory,
12
+ searchCatalog,
13
+ type CatalogSort,
12
14
  } from "../../wix/storefront/catalog";
13
15
  import type { Category, ProductSummary } from "../../wix/storefront/types";
14
16
 
17
+ export const SORTS = CATALOG_SORTS;
18
+
19
+ export interface ShopFilters {
20
+ minPrice?: number | string;
21
+ maxPrice?: number | string;
22
+ inStockOnly?: boolean;
23
+ /** Name search, max 100 chars. */
24
+ search?: string;
25
+ }
26
+
15
27
  export interface UseShopOptions {
16
28
  initialProducts?: ProductSummary[];
17
29
  initialCategories?: Category[];
18
- limit?: number;
30
+ pageSize?: number;
19
31
  }
20
32
 
21
33
  export interface UseShop {
@@ -25,21 +37,63 @@ export interface UseShop {
25
37
  /** null = "all products". */
26
38
  activeCategoryId: string | null;
27
39
  setActiveCategoryId: (id: string | null) => void;
40
+ sort: CatalogSort;
41
+ setSort: (sort: CatalogSort) => void;
42
+ filters: ShopFilters;
43
+ setFilters: (filters: ShopFilters) => void;
28
44
  loading: boolean;
29
45
  error: string | null;
46
+ retry: () => void;
47
+ hasMore: boolean;
48
+ loadMore: () => Promise<void>;
49
+ loadingMore: boolean;
50
+ }
51
+
52
+ interface PageState {
53
+ key: string | null;
54
+ products: ProductSummary[] | null;
55
+ cursor: string | null;
56
+ error: string | null;
57
+ loadingMore: boolean;
30
58
  }
31
59
 
32
- export function useShop({ initialProducts, initialCategories, limit = 100 }: UseShopOptions = {}): UseShop {
33
- const [products, setProducts] = useState<ProductSummary[] | null>(initialProducts ?? null);
60
+ export function useShop({
61
+ initialProducts,
62
+ initialCategories,
63
+ pageSize = 24,
64
+ }: UseShopOptions = {}): UseShop {
34
65
  const [categories, setCategories] = useState<Category[]>(initialCategories ?? []);
35
66
  const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
36
- const [loading, setLoading] = useState(false);
37
- const [error, setError] = useState<string | null>(null);
67
+ const [sort, setSort] = useState<CatalogSort>("featured");
68
+ const [filters, setFilters] = useState<ShopFilters>({});
69
+ const [attempt, setAttempt] = useState(0);
70
+ const [page, setPage] = useState<PageState>({
71
+ key: null,
72
+ products: initialProducts ?? null,
73
+ cursor: null,
74
+ error: null,
75
+ loadingMore: false,
76
+ });
77
+ const generation = useRef(0);
78
+ const pendingMore = useRef<object | null>(null);
79
+
80
+ const key = JSON.stringify([
81
+ pageSize,
82
+ activeCategoryId,
83
+ sort,
84
+ filters.minPrice ?? null,
85
+ filters.maxPrice ?? null,
86
+ !!filters.inStockOnly,
87
+ filters.search ?? "",
88
+ attempt,
89
+ ]);
90
+ const currentKey = useRef(key);
91
+ currentKey.current = key;
38
92
 
39
93
  useEffect(() => {
40
94
  if (initialCategories) return;
41
95
  let alive = true;
42
- fetchCategories().then((c) => alive && setCategories(c));
96
+ fetchCategories().then((list) => alive && setCategories(list)).catch(() => alive && setCategories([]));
43
97
  return () => {
44
98
  alive = false;
45
99
  };
@@ -47,34 +101,90 @@ export function useShop({ initialProducts, initialCategories, limit = 100 }: Use
47
101
  }, []);
48
102
 
49
103
  useEffect(() => {
50
- // The initial all-products data may have come from the server; only fetch when it
51
- // didn't, or when the buyer picked a category.
52
- if (activeCategoryId === null && initialProducts) {
53
- setProducts(initialProducts);
54
- return;
55
- }
104
+ const id = ++generation.current;
56
105
  let alive = true;
57
- setLoading(true);
58
- setError(null);
59
- const load = activeCategoryId
60
- ? fetchProductsByCategory(activeCategoryId, { limit })
61
- : fetchProducts({ limit });
62
- load
63
- .then((items) => {
64
- if (!alive) return;
65
- setProducts(items);
106
+ pendingMore.current = null;
107
+ // Keep the SSR seed (or the previous page) on screen while the fresh query runs;
108
+ // only a selection change with no seed shows skeletons.
109
+ setPage((p) => ({ key, products: p.key === null ? p.products : null, cursor: null, error: null, loadingMore: false }));
110
+ const [limit, categoryId, selectedSort, minPrice, maxPrice, inStockOnly, search] = JSON.parse(key);
111
+ searchCatalog({ limit, categoryId, sort: selectedSort, minPrice, maxPrice, inStockOnly, search })
112
+ .then((res) => {
113
+ if (alive && generation.current === id && currentKey.current === key) {
114
+ setPage({ key, products: res.products, cursor: res.nextCursor, error: null, loadingMore: false });
115
+ }
66
116
  })
67
117
  .catch((e) => {
68
- if (!alive) return;
69
- setProducts([]);
70
- setError(e instanceof Error ? e.message : String(e));
71
- })
72
- .finally(() => alive && setLoading(false));
118
+ if (alive && generation.current === id && currentKey.current === key) {
119
+ setPage({
120
+ key,
121
+ products: [],
122
+ cursor: null,
123
+ error: e instanceof Error ? e.message : "Couldn't load products.",
124
+ loadingMore: false,
125
+ });
126
+ }
127
+ });
73
128
  return () => {
74
129
  alive = false;
130
+ generation.current++;
75
131
  };
76
132
  // eslint-disable-next-line react-hooks/exhaustive-deps
77
- }, [activeCategoryId, limit]);
133
+ }, [key]);
134
+
135
+ const loadMore = useCallback(async () => {
136
+ if (page.key !== key || !page.cursor || pendingMore.current) return;
137
+ const request = { generation: generation.current, key };
138
+ pendingMore.current = request; // blocks repeated clicks before React rerenders
139
+ const isCurrent = () => generation.current === request.generation && currentKey.current === key;
140
+ setPage((p) => ({ ...p, loadingMore: true, error: null }));
141
+ try {
142
+ const res = await searchCatalog({ limit: pageSize, cursor: page.cursor });
143
+ if (isCurrent())
144
+ setPage((p) => {
145
+ const seen = new Set((p.products ?? []).map((product) => product.id));
146
+ return {
147
+ ...p,
148
+ products: [
149
+ ...(p.products ?? []),
150
+ ...res.products.filter((product) => {
151
+ if (seen.has(product.id)) return false;
152
+ seen.add(product.id);
153
+ return true;
154
+ }),
155
+ ],
156
+ cursor: res.nextCursor,
157
+ };
158
+ });
159
+ } catch (e) {
160
+ if (isCurrent())
161
+ setPage((p) => ({ ...p, error: e instanceof Error ? e.message : "Couldn't load more products." }));
162
+ } finally {
163
+ if (pendingMore.current === request) pendingMore.current = null;
164
+ if (isCurrent()) setPage((p) => ({ ...p, loadingMore: false }));
165
+ }
166
+ }, [key, page.key, page.cursor, pageSize]);
167
+
168
+ const retry = useCallback(() => setAttempt((n) => n + 1), []);
78
169
 
79
- return { products, categories, activeCategoryId, setActiveCategoryId, loading, error };
170
+ const current = page.key === key;
171
+ const seeded = page.key === null && page.products !== null; // SSR seed, first query in flight
172
+ // Seeded content on screen is not "loading" — skeletons are only for a genuinely empty wait.
173
+ const loading = (!current && !seeded) || (current && page.products === null);
174
+ return {
175
+ categories,
176
+ activeCategoryId,
177
+ setActiveCategoryId,
178
+ sort,
179
+ setSort,
180
+ filters,
181
+ setFilters,
182
+ products: current || seeded ? page.products : null,
183
+ loading,
184
+ error: current ? page.error : null,
185
+ retry,
186
+ hasMore: current && !!page.cursor,
187
+ loadMore,
188
+ loadingMore: current && page.loadingMore,
189
+ };
80
190
  }
@@ -72,8 +72,14 @@ export async function addLine(
72
72
  quantity = 1,
73
73
  extras?: AddToCartExtras,
74
74
  ): Promise<void> {
75
- await run(() => apiAdd(productId, variantId, quantity, extras));
76
- setState({ open: true });
75
+ try {
76
+ await run(() => apiAdd(productId, variantId, quantity, extras));
77
+ } finally {
78
+ // Open either way. On success this shows the new line; on refusal it's the only thing that
79
+ // makes .error visible — the drawer renders it, and a drawer that stays shut on failure
80
+ // turns a refused add (out of stock, a digital product with no file) into silence.
81
+ setState({ open: true });
82
+ }
77
83
  }
78
84
 
79
85
  export async function updateLineQuantity(lineItemId: string, quantity: number): Promise<void> {