@wix/agent-skills 1.20.0 → 1.20.1

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 (152) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-headless-fast/SKILL.md +200 -34
  3. package/skills/wix-headless-fast/entry/skill.md +10 -2
  4. package/skills/wix-headless-fast/install/attach.mjs +253 -0
  5. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  6. package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
  7. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
  8. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  9. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  10. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  11. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  12. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  13. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  14. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  15. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  16. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  17. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  18. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  19. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  20. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  21. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  22. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
  23. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  24. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  25. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  26. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  27. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  28. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  29. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  30. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  31. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  32. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  33. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  34. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  35. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  36. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
  37. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  38. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  39. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  40. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  41. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  42. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  43. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  44. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  45. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
  46. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  47. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  48. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  49. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  50. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  51. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  52. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  53. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  54. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  55. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  56. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  57. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  58. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  59. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  60. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
  61. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  62. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  63. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  64. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  65. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  66. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  67. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  68. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  69. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  70. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  71. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
  72. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  73. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  74. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  75. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  76. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  77. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  78. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  79. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  80. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
  81. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  82. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  83. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  84. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  85. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  86. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  87. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  88. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  89. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  90. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  91. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  92. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  93. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  94. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  95. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
  96. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  97. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  98. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  99. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  100. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  101. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  102. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  103. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  104. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  105. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  106. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  107. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  108. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  109. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  110. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
  111. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  112. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  113. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  114. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  115. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  116. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  117. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  118. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  119. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  120. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  121. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  122. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  123. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  124. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  125. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  126. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  127. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  128. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  129. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  130. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  131. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  132. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  133. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
  134. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  135. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  136. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  137. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  138. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  139. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  140. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
  141. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  142. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  143. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  144. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  145. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  146. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
  147. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
  148. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  149. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  150. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  151. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  152. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -0,0 +1,113 @@
1
+ // Availability, the booking form, and the createBooking → Cart V2 → checkout-or-place sequence over
2
+ // REST — the twin of app/wix/bookings/booking.ts. Same exports, same DTOs; every body comes from
3
+ // booking-core (the SAME file the SDK transport uses, deployed flat next to this one), so this file
4
+ // is only the transport: literal paths, one fetch per step. Failures are loud: a taken slot, a
5
+ // refused cart, a checkout that won't start all throw — surface the message to the visitor.
6
+ // All calls run with the visitor token: the booking and its cart are the token's (see ./client).
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slots.md
8
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-event-time-slots.md
9
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/create-booking.md
10
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
11
+ // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
12
+ // docs: https://dev.wix.com/docs/api-reference/crm/forms/form-schemas/get-form-summary.md
13
+ import { wixRequest } from "./client.js";
14
+ import {
15
+ FALLBACK_FIELDS,
16
+ appointmentSlotsRequest,
17
+ bookingCartRequest,
18
+ bookingIdOf,
19
+ bookingRequest,
20
+ cartIdOf,
21
+ cartTotal,
22
+ checkoutRedirectRequest,
23
+ checkoutRequired,
24
+ classSlotsRequest,
25
+ confirmedResult,
26
+ defaultTimeZone,
27
+ toFormFields,
28
+ toLocalDateString,
29
+ toSlot,
30
+ type Raw,
31
+ type SlotsWindow,
32
+ } from "./booking-core.js";
33
+ import type { BookingFormField, BookingResult, ServiceDetail, Slot } from "./types.js";
34
+
35
+ export { toLocalDateString };
36
+
37
+ /**
38
+ * Bookable slots for a service in [from, from + days) — APPOINTMENT and CLASS use different APIs;
39
+ * this branches for you. Dates are local wall-clock; timeZone defaults to the visitor's.
40
+ * APPOINTMENT: POST /service-availability/v2/time-slots { serviceId, fromLocalDate, toLocalDate, timeZone, bookable, cursorPaging, includeResourceTypeIds, resourceTypes? }
41
+ * CLASS: POST /service-availability/v2/time-slots/event { serviceIds, fromLocalDate, toLocalDate, timeZone, includeNonBookable, eventFilter? }
42
+ */
43
+ export async function fetchSlots(service: Pick<ServiceDetail, "id" | "type">, window: SlotsWindow = {}): Promise<Slot[]> {
44
+ const res =
45
+ service.type === "CLASS"
46
+ ? await wixRequest<Raw>("/service-availability/v2/time-slots/event", { body: classSlotsRequest(service.id, window) })
47
+ : await wixRequest<Raw>("/service-availability/v2/time-slots", { body: appointmentSlotsRequest(service.id, window) });
48
+ return ((res?.timeSlots ?? []) as Raw[]).map(toSlot);
49
+ }
50
+
51
+ /**
52
+ * The service's booking-form fields, flat and render-ready (values keyed by `target`). ALWAYS a
53
+ * non-empty list — contact basics when the schema is missing/unusable — so the form renders
54
+ * unconditionally. GET /form-schema-service/v4/forms/{formId}/summary (labels, types) and
55
+ * GET /form-schema-service/v4/forms/{formId} (which fields are required — the summary doesn't say).
56
+ */
57
+ export async function fetchBookingForm(formId: string | null): Promise<BookingFormField[]> {
58
+ if (!formId) return FALLBACK_FIELDS;
59
+ try {
60
+ const id = encodeURIComponent(formId);
61
+ const [res, form] = await Promise.all([
62
+ wixRequest<Raw>(`/form-schema-service/v4/forms/${id}/summary`, { method: "GET" }),
63
+ wixRequest<Raw>(`/form-schema-service/v4/forms/${id}`, { method: "GET" }).catch(() => null),
64
+ ]);
65
+ return toFormFields(res?.formSummary, form?.form);
66
+ } catch {
67
+ return FALLBACK_FIELDS;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Book a slot: createBooking → createCart (holds the seat) → calculateCart → hosted checkout (paid)
73
+ * or placeOrder (free / pay-in-person). Call from the browser. `formValues` is the object your
74
+ * inputs wrote, keyed by field `target` — passed as the formSubmission DIRECTLY. Throws with the
75
+ * refusal (slot taken, invalid form) — surface it, don't swallow it. `origin` is the site's real
76
+ * https origin as registered on the OAuth app's allowed domains (browser: window.location.origin).
77
+ * POST /bookings/v2/bookings { booking, formSubmission }
78
+ * POST /ecom/v2/carts { catalogItems: [{ quantity: 1, catalogReference: { catalogItemId: <bookingId>, appId } }], cart: { source: { channelType: "WEB" } } }
79
+ * POST /ecom/v2/carts/{cartId}/calculate {}
80
+ * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId: <cartId> }, callbacks: { postFlowUrl } }
81
+ * POST /ecom/v2/carts/{cartId}/place-order {}
82
+ */
83
+ export async function bookService(
84
+ service: ServiceDetail,
85
+ slot: Slot,
86
+ formValues: Record<string, unknown>,
87
+ {
88
+ staffId,
89
+ timeZone = defaultTimeZone(),
90
+ origin = typeof window !== "undefined" ? window.location.origin : "",
91
+ }: { staffId?: string; timeZone?: string; origin?: string } = {},
92
+ ): Promise<BookingResult> {
93
+ const created = await wixRequest<Raw>("/bookings/v2/bookings", {
94
+ body: { booking: bookingRequest(service, slot, { staffId, timeZone, idKey: "id" }), formSubmission: formValues },
95
+ });
96
+ const bookingId = bookingIdOf(created);
97
+ if (!bookingId) throw new Error("The booking couldn't be created — the slot may have just been taken.");
98
+
99
+ const newCart = await wixRequest<Raw>("/ecom/v2/carts", { body: bookingCartRequest(bookingId) });
100
+ const cartId = cartIdOf(newCart);
101
+ if (!cartId) throw new Error("The booking couldn't be reserved — please try again.");
102
+
103
+ const calc = await wixRequest<Raw>(`/ecom/v2/carts/${cartId}/calculate`, { body: {} });
104
+ if (checkoutRequired(service, cartTotal(calc))) {
105
+ const session = await wixRequest<Raw>("/headless/v1/redirect-session", { body: checkoutRedirectRequest(cartId, origin) });
106
+ const url = session?.redirectSession?.fullUrl;
107
+ if (!url) throw new Error("Checkout couldn't start — please try again.");
108
+ return { kind: "redirect", url };
109
+ }
110
+
111
+ const order = await wixRequest<Raw>(`/ecom/v2/carts/${cartId}/place-order`, { body: {} });
112
+ return confirmedResult(bookingId, order);
113
+ }
@@ -0,0 +1,55 @@
1
+ // Service reads over REST — the twin of app/wix/bookings/services.ts. Same exports, same DTOs; the
2
+ // rules and mappers come from services-core (the SAME file the SDK transport uses, deployed flat
3
+ // next to this one by deploy.mjs --stack static), so this file is only the transport: one fetch with
4
+ // a literal body per function. Every call here is safe from a browser with a visitor token.
5
+ // Porting: keep the paths, keep the bodies, port the core once.
6
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/query-services.md
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/categories-v2/query-categories.md
8
+ import { wixRequest } from "./client.js";
9
+ import { imgSrc } from "./media.js";
10
+ import {
11
+ BOOKINGS_APP_ID,
12
+ SERVICES_CONDITIONAL_FIELDS,
13
+ STAFF_RESOURCE_TYPE_ID,
14
+ isVisible,
15
+ servicesFilter,
16
+ toCategory,
17
+ toDetail,
18
+ toSummary,
19
+ type Raw,
20
+ } from "./services-core.js";
21
+ import type { BookingCategory, ServiceDetail, ServiceSummary } from "./types.js";
22
+
23
+ export { BOOKINGS_APP_ID, STAFF_RESOURCE_TYPE_ID };
24
+
25
+ // POST /bookings/v2/services/query { query: { filter, paging: { limit } }, conditionalFields }
26
+ async function queryServices(slug: string | undefined, limit: number): Promise<Raw[]> {
27
+ const res = await wixRequest<Raw>("/bookings/v2/services/query", {
28
+ body: { query: { filter: servicesFilter(slug), paging: { limit } }, conditionalFields: SERVICES_CONDITIONAL_FIELDS },
29
+ });
30
+ return (res?.services ?? []) as Raw[];
31
+ }
32
+
33
+ /** List bookable services (hidden ones filtered), mapped to grid-ready DTOs. */
34
+ export async function fetchServices({ limit = 100 } = {}): Promise<ServiceSummary[]> {
35
+ return (await queryServices(undefined, limit)).filter(isVisible).map((s) => toSummary(s, imgSrc));
36
+ }
37
+
38
+ /** One service by its URL slug (mainSlug.name) — a query with the slug in the filter. Null when not found. */
39
+ export async function fetchServiceBySlug(slug: string): Promise<ServiceDetail | null> {
40
+ const raw = (await queryServices(slug, 1))[0];
41
+ return raw ? toDetail(raw, imgSrc) : null;
42
+ }
43
+
44
+ /**
45
+ * Service categories for a filter bar — non-fatal (empty array on failure).
46
+ * POST /bookings/v2/categories/query { query: {} }
47
+ */
48
+ export async function fetchBookingCategories(): Promise<BookingCategory[]> {
49
+ try {
50
+ const res = await wixRequest<Raw>("/bookings/v2/categories/query", { body: { query: {} } });
51
+ return ((res?.categories ?? []) as Raw[]).map(toCategory).filter((c) => c.id);
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
@@ -51,6 +51,16 @@ image leaves that service text-only.
51
51
  **Seeding is additive — never delete or overwrite existing content**; ask first if a cleanup
52
52
  seems needed.
53
53
 
54
+ ## Supplied content
55
+
56
+ The general rules are in `references/shared/SUPPLIED-CONTENT.md`. For bookings, each service the
57
+ user lists is one entry. Its name becomes `name` and its length in minutes becomes `duration`. Its
58
+ price becomes `price`; a service marked free, or with no price, gets `"free": true`. A group activity
59
+ with a capacity or fixed times becomes a `CLASS` with `capacity` and one `sessions` entry per listed
60
+ time; everything else is an `APPOINTMENT`. A category or group becomes `category`. Their photo
61
+ becomes `imageUrl`. A service with no duration is a question for the user. Staff members and
62
+ opening hours are not seeded — say so; the owner sets them in the dashboard.
63
+
54
64
  ## Escape hatch — individual functions
55
65
  `setupBookings` composes exported steps — `installBookingsApp`, `queryStaffWithRetry`,
56
66
  `createCategories`, `createServices`, `scheduleClassSessions`, `importImage`,
@@ -1,27 +1,36 @@
1
1
  # CMS — playbook
2
2
 
3
- The data machinery ships as files — typed reads/writes over ANY collection, hooks, and the
4
- normalization layer (dates → ISO strings, `wix:image://` → https URLs), correct end-to-end.
5
- CMS is schema-generic, so unlike other verticals there are **no fixed pages**: the seed plan
6
- (`plan.json`) is the site's content model, and **you design the pages from it** — a listing
7
- surface and an item page per content collection, plus the home page and the brand. You never
8
- write data-access logic; you never skip designing.
3
+ The data machinery ships as files — typed reads/writes over ANY collection, the list and item
4
+ state machines, their React bindings, and the normalization layer (dates → ISO strings,
5
+ `wix:image://` → https URLs), correct end-to-end. CMS is schema-generic, so unlike other
6
+ verticals there are **no fixed pages**: the seed plan (`plan.json`) is the site's content model,
7
+ and **you design the pages from it** — a listing surface and an item page per content collection,
8
+ plus the home page and the brand. You never write data-access logic; you never skip designing.
9
9
 
10
10
  ## The file map (deployed into `src/`)
11
11
 
12
- **Don't read the shipped files** — this table and the contracts below are everything you
13
- need. Open a shipped file's source only on a real fallback (runtime error / uncovered field).
12
+ **On Astro and React the shipped files are tested and work as they are** — this table and the
13
+ contracts below are everything you need to use them, so don't spend the run reading their source;
14
+ wire them and build your surfaces. Reading them is the right move when something is off (a runtime
15
+ error, a field this playbook doesn't cover) or when the brief wants a behaviour they don't offer —
16
+ then read the file that owns it and change or extend it. On `lib`, `static`, and a port the hooks
17
+ don't deploy at all; the stores behind them do, and each wiring section below says how to bind
18
+ them. Files you edit: `SiteLayout.astro` and `styles/global.css`. Files you **create** (skeletons
19
+ below): the listing and item pages with their islands, plus your home page.
14
20
 
15
21
  | file | what it is |
16
22
  |---|---|
17
- | `wix/config.ts` · `wix/sdk.ts` · `wix/media.ts` · `wix/money.ts` | shared auth seam + helpers (deploy configures; nothing to set) |
23
+ | `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
24
+ | `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes)` — every `<img>` attribute for a DTO image (`src`, `srcSet`, `sizes`, lazy): `<img {...imgAttrs(item.photo, "33vw")} alt={item.title} />`; `{}` for an absent image — render your placeholder then |
18
25
  | `wix/cms/types.ts` | the DTOs (`CmsItem`, `CmsFilter`, `CmsSort`, `CmsQuery`, `CmsPage`) — contracts below |
19
- | `wix/cms/items.ts` | `queryItems`, `getItemById`, `getItemBy`, `countItems`, `insertItem`, `updateItem`, `patchItemFields`, `removeItem` |
20
- | `hooks/cms/useCollection.ts` | list state + skip paging — contract below |
21
- | `hooks/cms/useItem.ts` | one item by `_id` or slug field — contract below |
26
+ | `wix/cms/items.ts` | `queryItems`, `getItemById`, `getItemBy`, `countItems`, `insertItem`, `updateItem`, `patchItemFields`, `removeItem` — the transport; the rules and DTO mappers are in `items-core.ts` beside it (shared with the REST layer) |
27
+ | `wix/cms/items-core.ts` | the normalization rules, the filter guard, the wire spelling of a query, and `formatDate(iso)` — a DATE field as copy ("" for absent/invalid, never "Invalid Date") |
28
+ | `wix/cms/collection-store.ts` · `item-store.ts` | the list and item state machines, framework-free (`createCollectionStore()`, `createItemStore()` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
29
+ | `hooks/cms/useCollection.ts` | React binding of `collection-store.ts`: list state + skip paging — contract below |
30
+ | `hooks/cms/useItem.ts` | React binding of `item-store.ts`: one item by `_id` or slug field — contract below |
22
31
  | `styles/global.css` | the design system: Tailwind v4 + the `@theme` token block (shared across verticals) |
23
32
 
24
- There are **no shipped components** — the data layer and hooks are the machinery; every
33
+ There are **no shipped components** — the data layer, stores, and hooks are the machinery; every
25
34
  component is yours.
26
35
 
27
36
  Astro stack additionally gets:
@@ -48,14 +57,33 @@ pages bind to (collection ids and field keys verbatim).
48
57
  Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
49
58
  Style everything with Tailwind utilities on the tokens.
50
59
 
60
+ ### What a complete content site shows (recommended defaults)
61
+
62
+ Defaults for a brief that says nothing about them; the prompt wins where it differs. Look at the
63
+ seeded content before designing (how many collections, which fields, whether items carry images
64
+ and dates) and design for this content, not for a stereotype of the business.
65
+
66
+ - **Home:** what the site is about and one path into the content in the first screen; real items
67
+ under truthful headings; not a repeat of a listing page.
68
+ - **Listing:** a real card — image, title, a secondary field — in the first screen; each item a
69
+ link to its item page (`/<collection>/<slug>`); loading, empty, and error states that look
70
+ different; "load more" when `hasNext`.
71
+ - **Item page:** title, image, the date formatted, the body as HTML, related items only when a
72
+ reference field carries them; a slug that resolves to nothing shows only the not-found state.
73
+ - **Copy:** nothing the owner didn't supply — no invented fields, counts, or authors; no Wix IDs or
74
+ technical words in visible text.
75
+
51
76
  ### The contracts your components consume
52
77
 
78
+ Everything you need — tested and working as they are; read a file when something is off or the
79
+ brief wants more.
80
+
53
81
  ```ts
54
82
  // CmsItem — display-ready, fields FLAT on the item (never item.data.*):
55
83
  // { _id, _createdDate?, _updatedDate?, _owner?, ...fields }
56
84
  // TEXT/URL/EMAIL → string · NUMBER → number · BOOLEAN → boolean
57
- // DATE/DATETIME → ISO string (render: new Date(iso).toLocaleDateString())
58
- // IMAGE → resolved https URL (straight into <img src>; guard the absent case)
85
+ // DATE/DATETIME → ISO string (render: formatDate(item.publishDate) from wix/cms/items-core)
86
+ // IMAGE → resolved https URL (<img {...imgAttrs(item.photo, "33vw")} />; {} when absent → your placeholder)
59
87
  // RICH_TEXT → the stored HTML string (render via set:html on a wrapper you control)
60
88
  // REFERENCE/MULTI_REFERENCE → id(s); full CmsItem(s) when queried with include
61
89
 
@@ -81,40 +109,185 @@ Style everything with Tailwind utilities on the tokens.
81
109
  // removeItem(collectionId, id)
82
110
  ```
83
111
 
112
+ ### The pages and islands you create — skeletons
113
+
114
+ The class names in these skeletons are the Astro/React spelling of layout rules that hold on
115
+ every stack, and each rule is also named in words beside its classes. On a stack where the hooks
116
+ don't deploy (`lib`, `static`, a port), keep the rule and write it in your own CSS.
117
+
118
+ Each page is a thin SSR shell (fetch → DTO props → island, or no island when the page has no
119
+ interactivity); each island is a thin view over a hook. Hooks first, branches after. The islands
120
+ render on the server too (`client:load` SSRs), and by then the 200 and headers are already sent — a
121
+ render throw truncates the body mid-stream. Render every state totally; nothing in a render path may
122
+ throw.
123
+
124
+ ```astro
125
+ ---
126
+ // src/pages/recipes.astro — YOU create it (one per content collection). Items are fetched
127
+ // SERVER-SIDE (view-source shows titles) and handed to the island as serialized DTO props.
128
+ import SiteLayout from "../layouts/SiteLayout.astro";
129
+ import CollectionView from "../components/cms/CollectionView";
130
+ import { queryItems } from "../wix/cms/items";
131
+ import type { CmsPage } from "../wix/cms/types";
132
+ const SORT = [{ field: "publishDate", direction: "desc" as const }];
133
+ let page: CmsPage = { items: [], hasNext: false, total: null };
134
+ try {
135
+ page = await queryItems("recipes", { sort: SORT, limit: 12 });
136
+ } catch {
137
+ // Guarded: an unguarded SSR throw truncates the response mid-stream; the island renders your empty state.
138
+ }
139
+ ---
140
+ <SiteLayout title="Recipes">
141
+ <!-- your page heading / intro, then: -->
142
+ <CollectionView client:load collectionId="recipes" sort={SORT} limit={12} initialItems={page.items} initialHasNext={page.hasNext} />
143
+ </SiteLayout>
144
+ ```
145
+
146
+ ```astro
147
+ ---
148
+ // src/pages/recipes/[slug].astro — YOU create it. SEO is plain <title>/<meta name="description">
149
+ // from the DTO via the layout props — CMS collections have NO owner-editable SEO item type, so
150
+ // don't copy another vertical's wixMetadata/<SEO.Tags> wiring.
151
+ import SiteLayout from "../../layouts/SiteLayout.astro";
152
+ import { getItemBy } from "../../wix/cms/items";
153
+ import { formatDate } from "../../wix/cms/items-core";
154
+ import { imgAttrs } from "../../wix/media";
155
+ let item = null;
156
+ try {
157
+ item = await getItemBy("recipes", "slug", Astro.params.slug!, { include: ["categories"] });
158
+ } catch {
159
+ // Guarded: an unhandled SSR throw truncates the response mid-stream.
160
+ }
161
+ // A missing slug is a real 404 — never a fallback to another item.
162
+ if (!item) return new Response(null, { status: 404 });
163
+ ---
164
+ <SiteLayout title={String(item.title)} description={String(item.summary ?? "")}>
165
+ <!-- your detail layout: title; the image through imgAttrs (a placeholder when it returns {});
166
+ formatDate(item.publishDate); the RICH_TEXT body with set:html on a wrapper you control;
167
+ included reference items by name — never an id as content -->
168
+ </SiteLayout>
169
+ ```
170
+
171
+ ```tsx
172
+ // src/components/cms/CollectionView.tsx — YOU build it; a listing page mounts it.
173
+ import { useCollection } from "../../hooks/cms/useCollection";
174
+ import { formatDate } from "../../wix/cms/items-core";
175
+ import { imgAttrs } from "../../wix/media";
176
+ import type { CmsFilter, CmsItem, CmsSort } from "../../wix/cms/types";
177
+
178
+ export default function CollectionView(props: {
179
+ collectionId: string;
180
+ filters?: CmsFilter[];
181
+ sort?: CmsSort[];
182
+ limit?: number;
183
+ initialItems?: CmsItem[]; // SSR props from your page — pass straight to useCollection;
184
+ initialHasNext?: boolean; // omitted in a SPA (client fetch)
185
+ }) {
186
+ const { collectionId, ...options } = props;
187
+ const list = useCollection(collectionId, options);
188
+ // …you implement the render:
189
+ // • error → a short inline message
190
+ // • items === null → skeleton cards; [] → your honest empty state (no mock items)
191
+ // • else YOUR grid of YOUR cards: image via <img {...imgAttrs(item.photo, "(min-width: 768px) 33vw, 100vw")} alt={String(item.title)} />
192
+ // — src, srcSet, sizes and lazy loading in one spread; {} when the field is absent → your
193
+ // placeholder; title WRAPS (`min-w-0`, `break-words`), no `truncate`; the date as
194
+ // formatDate(item.publishDate); the card links to `/${collectionId}/${item.slug}`
195
+ // • hasNext → your "load more" control calling loadMore() (disabled while loadingMore)
196
+ }
197
+ ```
198
+
199
+ ### The reference files for stacks where the components don't deploy
200
+
201
+ On `lib`, `static`, and a port, nothing under `hooks/` arrives, and CMS ships no components. The
202
+ state machines behind the hooks do arrive — `wix/cms/collection-store.ts`, `item-store.ts` — so you
203
+ never rewrite them: create a store per surface, `subscribe`, render from `getState()`, call its
204
+ actions. Their `CollectionState` / `ItemState` interfaces are the render contract; read those. What
205
+ you write is the rendering — cards, the item view, the states — to the skeletons above. The two
206
+ hooks under `references/cms/app/hooks/cms/` are the binding pattern, ten lines each, if your
207
+ framework's external-store primitive is unfamiliar.
208
+
84
209
  ### Wiring — Astro (default)
85
210
 
86
- 1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass) and add one nav
87
- link per content surface.
88
- 2. Author your pages under `src/pages/` — SSR fetch in the frontmatter, DTO props to your
89
- islands (`client:load`; a page with no interactivity needs no island at all). Listing:
90
-
91
- ```astro
92
- ---
93
- import SiteLayout from "../layouts/SiteLayout.astro";
94
- import { queryItems } from "../wix/cms/items";
95
- import type { CmsPage } from "../wix/cms/types";
96
- let page: CmsPage = { items: [], hasNext: false, total: null };
97
- try {
98
- page = await queryItems("recipes", { sort: [{ field: "publishDate", direction: "desc" }] });
99
- } catch {
100
- // an unguarded SSR throw truncates the response mid-stream
101
- }
102
- ---
103
- ```
104
-
105
- Item page (`src/pages/recipes/[slug].astro`): `getItemBy("recipes", "slug", Astro.params.slug!)`
106
- in the frontmatter; `return new Response(null, { status: 404 })` on null. SEO is plain
107
- `<title>`/`<meta name="description">` from the DTO via the layout props — CMS collections
108
- have **no owner-editable SEO item type**, so don't copy another vertical's
109
- `wixMetadata`/`<SEO.Tags>` wiring.
110
- 3. **Author your surfaces in as few messages as possible** — batch multiple Writes per
111
- message (components and pages are independent files).
211
+ 1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass) and add one nav link
212
+ per content surface.
213
+ 2. Author your pages under `src/pages/` per the skeletons above — SSR fetch in the frontmatter,
214
+ DTO props to your islands (`client:load`; a page with no interactivity needs no island at all).
215
+ 3. Write `pages/index.astro` (home) on `SiteLayout`.
216
+ 4. **Author your surfaces in as few messages as possible** — batch multiple Writes per message
217
+ (components and pages are independent files).
218
+
219
+ ### Wiring — another JS framework (`--stack lib`: Vue, Svelte, Solid, plain Vite)
220
+
221
+ `deploy.mjs cms --stack lib` put the data layer in `src/wix/` and nothing else: `sdk.ts` (the
222
+ visitor client, configured with the public client id), `media.ts`, `money.ts`, and `wix/cms/` —
223
+ `items.ts`, `types.ts`, `items-core.ts`, and the two stores `collection-store.ts`, `item-store.ts`.
224
+ None of it is React. The hooks don't ship on this stack; the stores replace them, and you write the
225
+ components in your framework to the contracts on this page:
226
+
227
+ - bind the stores with your framework's external-store primitive (Vue: `shallowRef` updated in
228
+ `subscribe`; Svelte: `readable(store.getState(), (set) => store.subscribe(() => set(store.getState())))`;
229
+ Solid: a signal set in `subscribe`). `createCollectionStore({ collectionId, filters?, sort?, limit?, include?, initialItems? })`
230
+ per listing (`start()` when mounted, `stop()` when unmounted, `setQuery()` when filters change,
231
+ `loadMore()`), `createItemStore({ collectionId, ref: { id } | { by }, initialItem? })` per item
232
+ surface. State in, actions out — exactly the hooks' contracts above.
233
+
234
+ Routes `/<collection>`, `/<collection>/:slug` (via `getItemBy`, null → your 404); dev server on
235
+ 4321; a static build goes through `npx @wix/cli@latest release` with `site.outputDirectory`
236
+ pointing at the build folder, an SSR build is hosted by you. Page tags from the item's own fields.
237
+
238
+ ### Wiring — static site (`--stack static`, no bundler)
239
+
240
+ `deploy.mjs cms --stack static --out site` put the REST layer in `site/js/wix/` (browser ESM, the
241
+ `.ts` beside each `.js` for reading). Everything the visitor loads lives under `site/` — pages,
242
+ styles, `js/` — and `wix.config.json`'s `site.outputDirectory` is `"./site"`; the project root
243
+ (config, plan, seed output) is never the upload. Same function names and DTOs as the table above,
244
+ so the contracts on this page hold unchanged: `queryItems`, `getItemBy`, `getItemById`,
245
+ `countItems` (and the writes) from `./js/wix/items.js`; `formatDate` from `./js/wix/items-core.js`;
246
+ `imgAttrs` from `./js/wix/media.js`. The state machines ship too: `createCollectionStore` from
247
+ `./js/wix/collection-store.js` (the listing — `start()` once the page is up, `loadMore()`,
248
+ `setQuery()`), `createItemStore` from `./js/wix/item-store.js` (the item page — `notFound` is your
249
+ 404 state). No components ship — you write the rendering in plain JS: one render function per
250
+ surface that reads `getState()`, called from `subscribe`, with the surface's controls calling the
251
+ store's actions. Pages are `<collection>.html` and `item.html?collection=…&slug=…` (Wix static
252
+ hosting serves files, not directories — name the file and link to it). Set `document.title` and
253
+ the meta description from the item's fields once it loads. The visitor token persists in
254
+ `localStorage` on its own; never mint per page. Reads work on any collection whose `read` is
255
+ `ANYONE`; a write from the page succeeds only where the seed opened that verb (403 → a permissions
256
+ step). `npx @wix/cli@latest release` uploads `site/`.
257
+
258
+ ### Wiring — server-rendered, another language (Flask, Laravel, Rails, …)
259
+
260
+ Run `deploy.mjs cms --stack static` in the project folder anyway: `js/wix/` is both the
261
+ browser-side code and the readable spec. Then split by where the call runs. **Reads on the
262
+ server:** port `js/wix/items.ts` and `items-core.ts` to your language — `queryItems`, `getItemBy`,
263
+ `getItemById`, `countItems` returning the same DTO shapes as dicts (the core's `toValue` rules:
264
+ `{ "$date": iso }` → ISO string, `wix:image://` → the https URL form in `media.ts`), one anonymous
265
+ visitor token per process for these public reads (mint and refresh per `client.ts`) — and render
266
+ the listing and item pages in your templates to the contracts above, so titles are in the HTML;
267
+ page tags from the item's fields. **Visitor writes in the browser** (a collection the seed opened
268
+ to visitors or members): `insertItem` from `./js/wix/items.js` on the page, exactly as the static
269
+ wiring above — the browser owns the visitor token, so the server handles no per-visitor tokens.
270
+ Routes stay `/<collection>`, `/<collection>/<slug>`.
271
+
272
+ **Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted.** Same port for the
273
+ reads, run at build time with one anonymous token; the generator must emit every item page (walk
274
+ `queryItems` by `skip` until `hasNext` is false — never only the first page). Run
275
+ `deploy.mjs cms --stack static --out <build dir>` so `js/wix/` is inside the output the pages
276
+ import from, point `site.outputDirectory` at that folder, `wix release`. Pages sit at different
277
+ depths (`/`, `/<collection>/…`): give the templates one base path to `js/wix/` (a template
278
+ variable, or root-relative `/js/wix/…`), never a relative `./js/wix/` — it breaks one level down.
279
+ The frozen listing is the first paint; a filter or "load more" still runs client-side on it
280
+ through `createCollectionStore()` from `./js/wix/collection-store.js`, exactly as on a static
281
+ site. Close with the live URL, the rebuild + release command, and one line for the owner:
282
+ dashboard edits to the content reach the site when that command runs.
112
283
 
113
284
  ### Wiring — React SPA (Vite etc.)
114
285
 
115
286
  Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
116
287
  plugins — deploy added the dep). Routes are yours: a list route per collection on
117
- `useCollection`, a detail route on `useItem` with `by: { field: "slug", value }`.
288
+ `useCollection`, a detail route on `useItem` with `by: { field: "slug", value }` (components
289
+ fetch client-side when no `initialItems` / `initialItem` is passed). Deploy wrote the public
290
+ client id into `wix/config.ts`; nothing else to configure.
118
291
 
119
292
  ## Hard rules
120
293
 
@@ -132,7 +305,8 @@ plugins — deploy added the dep). Routes are yours: a list route per collection
132
305
  or use `patchItemFields`. Date fields on any write are Date objects — a round-tripped ISO
133
306
  string is silently stored as text and breaks date queries.
134
307
  - **Writes 403 unless the collection was seeded with that verb open** — surface a 403 as a
135
- permissions setup step (which permission, where in the dashboard), not a code bug.
308
+ permissions setup step (which permission, where in the dashboard), not a code bug. Anything
309
+ that needs elevated access runs server-side per `references/shared/CUSTOM_OPERATIONS.md`.
136
310
  - **User-created content is a CMS contribution flow.** For a member or visitor submission that
137
311
  the app must later list, use the seeded CMS collection and its intended permission preset.
138
312
  If the operation needs elevated access — especially a browser-created file that must enter
@@ -145,17 +319,31 @@ plugins — deploy added the dep). Routes are yours: a list route per collection
145
319
  returned Wix Media reference. Do not author another upload endpoint for that flow.
146
320
  - **RICH_TEXT is HTML, not plain text** — render it with `set:html` (Astro) /
147
321
  `dangerouslySetInnerHTML` (React) on a wrapper; never interpolate it as text.
322
+ - **Dates through `formatDate`**, images through `imgAttrs` — the DTO's ISO string and https URL
323
+ are data, not copy.
148
324
  - Reference fields hold ids unless the query passed `include` — don't render an id as
149
325
  content.
150
- - Guard absent IMAGE fields — render a fallback, never an empty/broken `<img>`.
151
- - Theme via the `@theme` tokens; no parallel theme files, no hardcoded palettes.
326
+ - Guard absent IMAGE fields — `imgAttrs` returns `{}`; render a fallback, never an empty/broken `<img>`.
327
+ - **Page at the source, not on a loaded list.** `hasNext`/`loadMore()` fetch the next page from
328
+ Wix; never raise the page limit instead of paging, never filter or sort client-side a page you
329
+ already hold — pass `filters`/`sort` and let Wix apply them across the collection.
330
+ - **Call every hook before any conditional return.** An island that returns early for
331
+ `notFound`/loading above its hooks changes hook order between renders and React throws.
332
+ - Where the shipped code deploys with Tailwind (Astro, React): theme via the `@theme` tokens, your
333
+ markup on the same tokens; no parallel theme files, no hardcoded palettes. Where it doesn't
334
+ (`lib`, `static`, a port): style with whatever your stack does well, on one token set of your
335
+ own; the rule that survives is the token set, not Tailwind.
152
336
  - Live data or an honest empty state — never mock items or invent fields not in the plan.
153
337
 
154
338
  ## Point the user to their dashboard
155
339
 
156
- Give the owner the dashboard link — the deploy step's JSON printed `dashboardUrl`; append
157
- `/wix-cms` for the collections area (items, fields, and More Actions → Permissions &
158
- Privacy live there).
340
+ Hand the owner these links — `{siteId}` is `siteId` in `wix.config.json` (the deploy JSON prints it as
341
+ `dashboardUrl`); a collection id is its `_id` (the seed result lists them).
342
+
343
+ | page | `https://manage.wix.com/dashboard/{siteId}/` + |
344
+ |---|---|
345
+ | Collections | `wix-cms` |
346
+ | A collection's items (fields, and More Actions → Permissions & Privacy) | `wix-cms/data/{collectionId}` |
159
347
 
160
348
  ## Seeding
161
349
 
@@ -163,17 +351,3 @@ Per `seed/SEED.md` — plain-data `plan.json` into `seed-cms.mjs` from the proje
163
351
  plan is the content model your pages bind to: design collections that exercise the UI
164
352
  (a `slug` field per detail page, an IMAGE field with a verified `imageUrl` per item, a DATE
165
353
  field when the content is chronological).
166
-
167
- ## Verify (before declaring done)
168
-
169
- - [ ] Each listing page renders live items SSR (view-source shows titles) through YOUR
170
- components; an empty collection shows your honest empty state (no mock items).
171
- - [ ] Item pages resolve by slug with a real 404 on a bad slug.
172
- - [ ] Images render (resolved https URLs, no raw `wix:image://`); absent images fall back
173
- gracefully.
174
- - [ ] Dates render formatted (no "Invalid Date", no raw ISO strings in copy).
175
- - [ ] RICH_TEXT renders formatted (no visible HTML tags, no `[object Object]`).
176
- - [ ] Any 403 surfaced as a permissions step with the dashboard link.
177
- - [ ] Listing/item/home surfaces are YOUR designs on the tokens; data-layer/hook files
178
- unedited.
179
- - [ ] Dashboard link handed to the owner.
@@ -1,9 +1,11 @@
1
- // One collection as list state. SSR-friendly: pass server-fetched items as `initialItems`
2
- // (Astro frontmatter / server component) and no client fetch happens for the first page; a
3
- // SPA passes nothing. Changing filters/sort (by value) refetches; loadMore() appends the
4
- // next page (skip paging).
5
- import { useCallback, useEffect, useRef, useState } from "react";
6
- import { queryItems } from "../../wix/cms/items";
1
+ // React binding of the collection store (wix/cms/collection-store.ts) — the list state machine
2
+ // lives there, framework-free; this hook subscribes to one instance per mounted listing. SSR-friendly:
3
+ // pass server-fetched items as `initialItems` (Astro frontmatter / server component) and no client
4
+ // fetch happens for the first page; a SPA passes nothing. Changing filters/sort (by value) refetches;
5
+ // loadMore() appends the next page (skip paging). Astro islands and React SPAs use this; a static
6
+ // page, Vue, or Svelte uses the store directly.
7
+ import { useEffect, useRef, useSyncExternalStore } from "react";
8
+ import { createCollectionStore, type CollectionStore } from "../../wix/cms/collection-store";
7
9
  import type { CmsFilter, CmsItem, CmsSort } from "../../wix/cms/types";
8
10
 
9
11
  export interface UseCollectionOptions {
@@ -30,54 +32,19 @@ export interface UseCollection {
30
32
 
31
33
  export function useCollection(collectionId: string, options: UseCollectionOptions = {}): UseCollection {
32
34
  const { filters, sort, limit = 20, include, initialItems, initialHasNext } = options;
35
+ const ref = useRef<CollectionStore | null>(null);
36
+ if (!ref.current) ref.current = createCollectionStore({ collectionId, filters, sort, limit, include, initialItems, initialHasNext });
37
+ const store = ref.current;
33
38
  // Query identity by value — Dates in filters serialize to ISO, so this is stable.
34
39
  const key = JSON.stringify([collectionId, filters ?? null, sort ?? null, limit, include ?? null]);
35
- const initialKey = useRef<string | null>(initialItems ? key : null);
36
- const [items, setItems] = useState<CmsItem[] | null>(initialItems ?? null);
37
- const [hasNext, setHasNext] = useState<boolean>(
38
- initialHasNext ?? (initialItems ? initialItems.length >= limit : false),
39
- );
40
- const [loadingMore, setLoadingMore] = useState(false);
41
- const [error, setError] = useState<string | null>(null);
42
- const itemsRef = useRef<CmsItem[] | null>(items);
43
- itemsRef.current = items;
44
-
45
40
  useEffect(() => {
46
- if (initialKey.current === key) return; // the SSR pass already answered this exact query
47
- initialKey.current = null;
48
- let alive = true;
49
- setItems(null);
50
- setError(null);
51
- queryItems(collectionId, { filters, sort, limit, include })
52
- .then((page) => {
53
- if (!alive) return;
54
- setItems(page.items);
55
- setHasNext(page.hasNext);
56
- })
57
- .catch((e) => {
58
- if (!alive) return;
59
- setItems([]);
60
- setError(e instanceof Error ? e.message : String(e));
61
- });
62
- return () => {
63
- alive = false;
64
- };
65
- // eslint-disable-next-line react-hooks/exhaustive-deps
66
- }, [key]);
67
-
68
- const loadMore = useCallback(() => {
69
- const current = itemsRef.current;
70
- if (!current || loadingMore) return;
71
- setLoadingMore(true);
72
- queryItems(collectionId, { filters, sort, limit, include, skip: current.length })
73
- .then((page) => {
74
- setItems([...(itemsRef.current ?? []), ...page.items]);
75
- setHasNext(page.hasNext);
76
- })
77
- .catch((e) => setError(e instanceof Error ? e.message : String(e)))
78
- .finally(() => setLoadingMore(false));
41
+ store.setQuery({ collectionId, filters, sort, limit, include });
79
42
  // eslint-disable-next-line react-hooks/exhaustive-deps
80
- }, [key, loadingMore]);
81
-
82
- return { items, hasNext, loadMore, loadingMore, error };
43
+ }, [store, key]);
44
+ useEffect(() => {
45
+ store.start();
46
+ return () => store.stop();
47
+ }, [store]);
48
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
49
+ return { items: state.items, hasNext: state.hasNext, loadMore: store.loadMore, loadingMore: state.loadingMore, error: state.error };
83
50
  }