@wix/agent-skills 1.6.0 → 1.8.0
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.
- package/README.md +5 -9
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +51 -42
- package/skills/wix-app/references/BACKEND_EVENT.md +10 -65
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +101 -406
- package/skills/wix-app/references/DASHBOARD_MENU_PLUGIN.md +14 -111
- package/skills/wix-app/references/DASHBOARD_MODAL.md +56 -205
- package/skills/wix-app/references/DASHBOARD_PAGE.md +19 -98
- package/skills/wix-app/references/DASHBOARD_PLUGIN.md +6 -108
- package/skills/wix-app/references/DATA_COLLECTION.md +113 -198
- package/skills/wix-app/references/EMBEDDED_SCRIPT.md +41 -268
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +27 -76
- package/skills/wix-app/references/SERVICE_PLUGIN.md +35 -188
- package/skills/wix-app/references/SITE_PLUGIN.md +8 -92
- package/skills/wix-app/references/STORES_VERSIONING.md +1 -0
- package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +0 -44
- package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +19 -330
- package/skills/wix-app/references/dashboard-menu-plugin/bookings-slots.md +0 -12
- package/skills/wix-app/references/dashboard-menu-plugin/ecommerce-slots.md +7 -0
- package/skills/wix-app/references/dashboard-menu-plugin/restaurants-slots.md +0 -12
- package/skills/wix-app/references/dashboard-plugin/SLOTS.md +3 -15
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +2 -18
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +2 -42
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +1 -11
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +1 -11
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +2 -40
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +2 -19
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +2 -18
- package/skills/wix-app/references/site-plugin/SLOTS.md +17 -68
- package/skills/wix-design-system/SKILL.md +4 -0
- package/skills/wix-headless/SKILL.md +131 -386
- package/skills/wix-headless/references/BUILD-astro.md +246 -0
- package/skills/wix-headless/references/BUILD-own-build.md +167 -0
- package/skills/wix-headless/references/BUILD.md +105 -0
- package/skills/wix-headless/references/DESIGN_SYSTEM.md +175 -0
- package/skills/wix-headless/references/DISCOVERY-connect.md +55 -0
- package/skills/wix-headless/references/DISCOVERY-create.md +238 -0
- package/skills/wix-headless/references/DISCOVERY.md +50 -306
- package/skills/wix-headless/references/PLAN-connect.md +19 -0
- package/skills/wix-headless/references/PLAN-create.md +17 -0
- package/skills/wix-headless/references/PLAN.md +92 -0
- package/skills/wix-headless/references/SEED.md +214 -0
- package/skills/wix-headless/references/SETUP.md +73 -248
- package/skills/wix-headless/references/{blog → astro/blog}/BLOG_PAGES.md +6 -5
- package/skills/wix-headless/references/{blog → astro/blog}/BLOG_SETUP.md +2 -2
- package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +337 -0
- package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +428 -0
- package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +214 -0
- package/skills/wix-headless/references/{cms → astro/cms}/FAQ_KNOWLEDGE_BASE.md +14 -12
- package/skills/wix-headless/references/{cms → astro/cms}/PORTFOLIO.md +15 -16
- package/skills/wix-headless/references/{cms → astro/cms}/RESOURCE_LIBRARY.md +13 -15
- package/skills/wix-headless/references/{cms → astro/cms}/TEAM_DIRECTORY.md +11 -11
- package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +354 -0
- package/skills/wix-headless/references/{ecom → astro/ecom}/CART_PAGES.md +8 -9
- package/skills/wix-headless/references/{ecom → astro/ecom}/CART_WIRING.md +6 -6
- package/skills/wix-headless/references/{ecom → astro/ecom}/COMPONENTS_CSS.md +6 -6
- package/skills/wix-headless/references/{forms → astro/forms}/CONTACT_FORM.md +18 -32
- package/skills/wix-headless/references/{gift-cards → astro/gift-cards}/COMPONENTS.md +3 -3
- package/skills/wix-headless/references/{gift-cards → astro/gift-cards}/PAGES.md +2 -2
- package/skills/wix-headless/references/{stores → astro/stores}/BACK_IN_STOCK.md +7 -7
- package/skills/wix-headless/references/{stores → astro/stores}/CATEGORY_PAGES.md +12 -13
- package/skills/wix-headless/references/{stores → astro/stores}/COMPONENTS_CSS.md +7 -7
- package/skills/wix-headless/references/{stores → astro/stores}/HOME_AND_NAV.md +11 -9
- package/skills/wix-headless/references/{stores → astro/stores}/PRODUCT_PAGES.md +11 -9
- package/skills/wix-headless/references/{stores → astro/stores}/SHARED_WIRING.md +8 -7
- package/skills/wix-headless/references/astro/templates/Footer.astro +27 -0
- package/skills/wix-headless/references/astro/templates/Layout.astro +132 -0
- package/skills/wix-headless/references/astro/templates/Navigation.astro +38 -0
- package/skills/wix-headless/references/astro/templates/astro.config.mjs +41 -0
- package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +218 -0
- package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +194 -0
- package/skills/wix-headless/references/astro/templates/bookings/ManageBooking.tsx +84 -0
- package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +25 -0
- package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +50 -0
- package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +45 -0
- package/skills/wix-headless/references/astro/templates/bookings/api/confirm-booking.ts +34 -0
- package/skills/wix-headless/references/astro/templates/bookings/api/waitlist.ts +74 -0
- package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +111 -0
- package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +413 -0
- package/skills/wix-headless/references/astro/templates/bookings/manage-booking.astro +76 -0
- package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +81 -0
- package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +65 -0
- package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +302 -0
- package/skills/wix-headless/references/astro/templates/global.css +286 -0
- package/skills/wix-headless/references/astro/templates/index.astro +56 -0
- package/skills/wix-headless/{templates → references/astro/templates}/stores/back-in-stock.ts +1 -1
- package/skills/wix-headless/{templates → references/astro/templates}/stores/components-stores.css +138 -33
- package/skills/wix-headless/references/blog/BLOG_CONTENT.md +28 -19
- package/skills/wix-headless/references/blog/INSTRUCTIONS.md +6 -6
- package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +106 -0
- package/skills/wix-headless/references/bookings/SERVICES_DATA.md +295 -0
- package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +35 -232
- package/skills/wix-headless/references/cms/INSTRUCTIONS.md +39 -8
- package/skills/wix-headless/references/commands/known-apps.json +2 -1
- package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +142 -0
- package/skills/wix-headless/references/custom/INSTRUCTIONS.md +88 -0
- package/skills/wix-headless/references/custom/blog/WIRING.md +52 -0
- package/skills/wix-headless/references/custom/bookings/WIRING.md +88 -0
- package/skills/wix-headless/references/custom/cms/WIRING.md +97 -0
- package/skills/wix-headless/references/custom/ecom/WIRING.md +67 -0
- package/skills/wix-headless/references/custom/forms/WIRING.md +101 -0
- package/skills/wix-headless/references/custom/stores/WIRING.md +80 -0
- package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +12 -12
- package/skills/wix-headless/references/forms/INSTRUCTIONS.md +6 -6
- package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +13 -13
- package/skills/wix-headless/references/images/INSTRUCTIONS.md +58 -60
- package/skills/wix-headless/references/seed-recipes.md +49 -0
- package/skills/wix-headless/references/shared/AGENTS_MD.md +47 -0
- package/skills/wix-headless/references/shared/AUTHENTICATION.md +108 -0
- package/skills/wix-headless/references/shared/BUILD_NOISE.md +2 -2
- package/skills/wix-headless/references/shared/DESIGN_MD.md +85 -0
- package/skills/wix-headless/references/shared/DOCS_SEARCH.md +41 -0
- package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +40 -39
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +92 -41
- package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +3 -3
- package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +32 -312
- package/skills/wix-headless/references/shared/STYLING.md +48 -21
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +59 -29
- package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +31 -28
- package/skills/wix-headless/references/verticals/_schema.json +31 -117
- package/skills/wix-headless/references/verticals/_schema.md +60 -150
- package/skills/wix-headless/references/verticals/blog.md +5 -99
- package/skills/wix-headless/references/verticals/bookings.md +41 -0
- package/skills/wix-headless/references/verticals/cms.md +9 -100
- package/skills/wix-headless/references/verticals/ecom.md +6 -77
- package/skills/wix-headless/references/verticals/forms.md +5 -85
- package/skills/wix-headless/references/verticals/gift-cards.md +6 -103
- package/skills/wix-headless/references/verticals/stores.md +10 -144
- package/skills/wix-headless/scripts/check-manifest.mjs +148 -11
- package/skills/wix-headless/scripts/compose.mjs +540 -0
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +136 -53
- package/skills/wix-headless/scripts/flatten.sh +61 -0
- package/skills/wix-headless/scripts/generate-images.mjs +242 -0
- package/skills/wix-headless/scripts/patch-decorative-slots.mjs +67 -43
- package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +3 -3
- package/skills/wix-headless/scripts/scaffold.sh +113 -27
- package/skills/wix-headless/scripts/seed-utilities.sh +50 -12
- package/skills/wix-headless/scripts/write-decorative-json.mjs +62 -0
- package/skills/wix-manage/SKILL.md +4 -8
- package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +2 -0
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +4 -0
- package/skills/wix-manage/references/cms/cms-references-and-relationships.md +2 -0
- package/skills/wix-manage/references/cms/cms-schema-management.md +4 -0
- package/skills/wix-manage/references/domains/domain-search-and-purchase.md +10 -7
- package/skills/wix-manage/references/ecommerce/api-shipping.md +443 -0
- package/skills/wix-manage/references/ecommerce/flow-add-free-shipping.md +5 -6
- package/skills/wix-manage/references/ecommerce/flow-fix-coverage-gaps.md +4 -5
- package/skills/wix-manage/references/ecommerce/flow-optimize-shipping-rates.md +4 -5
- package/skills/wix-manage/references/ecommerce/goal-increase-aov.md +1 -1
- package/skills/wix-manage/references/ecommerce/goal-reduce-cart-abandonment.md +1 -2
- package/skills/wix-manage/references/ecommerce/guardrail-shipping-health.md +2 -2
- package/skills/wix-manage/references/ecommerce/recipe-apply-shipping-recommendations.md +4 -5
- package/skills/wix-manage/references/ecommerce/recommend-ecommerce-strategy.md +7 -7
- package/skills/wix-manage/references/ecommerce/setup-discount-rules.md +209 -72
- package/skills/wix-manage/references/ecommerce/setup-store-pickup-location.md +9 -10
- package/skills/wix-manage/references/ecommerce/skill-graph.md +4 -4
- package/skills/wix-manage/references/forms/create-form.md +50 -0
- package/skills/wix-manage/references/stores/bulk-create-products-with-options.md +2 -0
- package/skills/wix-manage/references/stores/create-product-from-image.md +91 -38
- package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +2 -0
- package/skills/wix-app/references/custom-element-widget/default-thumbnail.png +0 -0
- package/skills/wix-app/references/data-collection/extension-template.ts +0 -107
- package/skills/wix-headless/references/ORCHESTRATION.md +0 -269
- package/skills/wix-headless/references/commands/install-app.md +0 -42
- package/skills/wix-headless/references/commands/mcp-bootstrap.md +0 -28
- package/skills/wix-headless/references/designer/INSTRUCTIONS.md +0 -828
- package/skills/wix-headless/references/shared/MCP_PREFIX.md +0 -105
- package/skills/wix-headless/scripts/copy-utility-templates.mjs +0 -153
- package/skills/wix-headless/scripts/finalize-run-json.mjs +0 -141
- package/skills/wix-headless/scripts/init-site-json.mjs +0 -63
- package/skills/wix-headless/scripts/preview.sh +0 -45
- package/skills/wix-headless/scripts/release.sh +0 -73
- package/skills/wix-headless/templates/ecom/components-ecom.css +0 -215
- package/skills/wix-manage/references/events/list-events.md +0 -203
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/CartBadge.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/CartView.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/cart.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/discounts.ts +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/ecom/thank-you.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/GiftCardPurchase.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/_home-teaser-snippet.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/_nav-snippet.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/components-gift-cards.css +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/gift-cards.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/gift-cards/gift-cards.ts +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/AddToCartButton.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/BackInStockForm.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/CategoryRail.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/ProductCard.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/ProductPurchase.tsx +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/SeoTags.astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/categories.ts +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/category/[slug].astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/products/[slug].astro +0 -0
- /package/skills/wix-headless/{templates → references/astro/templates}/stores/products/index.astro +0 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# Bookings Pages — Phase 4
|
|
2
|
+
|
|
3
|
+
You are the Phase 4 Pages agent for the bookings vertical. Your scope: write the route files. Read `references/shared/IMPLEMENTER.md` + `references/shared/STYLING.md` first.
|
|
4
|
+
|
|
5
|
+
> **Start from the canonical templates — don't invent the SSR data access.** Verified templates live at `<SKILL_ROOT>/references/astro/templates/bookings/`: `services/index.astro`, `services/[slug].astro`, `booking-confirmation.astro`, `manage-booking.astro` (plus the pre-copied API endpoints `api/confirm-booking.ts`, `api/waitlist.ts`). **Read and adapt them** (copy, headings, styling) — this doc explains the *why*. The SSR query pattern (`@wix/essentials` `auth.elevate`), the confirm step, and the manage/waitlist endpoints are live-verified.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Files you own
|
|
10
|
+
|
|
11
|
+
| File | Route | Description |
|
|
12
|
+
|------|-------|-------------|
|
|
13
|
+
| `src/pages/services/index.astro` | `/services` | SSR services listing grid |
|
|
14
|
+
| `src/pages/services/[slug].astro` | `/services/[slug]` | SSR service detail + client-side booking flow |
|
|
15
|
+
| `src/pages/booking-confirmation.astro` | `/booking-confirmation` | Status-aware confirmation + inline cancel |
|
|
16
|
+
| `src/pages/manage-booking.astro` | `/manage-booking` | Per-booking view/cancel (anonymous token) |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 1 — Read your seeded slice
|
|
21
|
+
|
|
22
|
+
Read your `seeded.bookings` slice from **`.wix/seeded.json`** (written once by the orchestrator at the seed gate — see `BUILD-astro.md` § "The `.wix/seeded.json` handoff"): `services[{id, slug, name, type, durationMinutes, price, currency}]` + `staff[{id, resourceId, name}]`. Use it to know the seeded slugs/names/types while authoring — do **NOT** import it into route files; the pages query the **live SDK** at request time (below). If your slice is absent, fail loud (`status: "partial"`, `errors: [{code: "SEEDED_JSON_SLICE_MISSING", missing: "seeded.bookings"}]`) — do not render an empty page.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Page 1 — `/services/index.astro`
|
|
27
|
+
|
|
28
|
+
### Frontmatter
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
---
|
|
32
|
+
import Layout from '../../layouts/Layout.astro';
|
|
33
|
+
import ServiceCard from '../../components/ServiceCard.astro';
|
|
34
|
+
import { services } from '@wix/bookings';
|
|
35
|
+
import { auth } from '@wix/essentials';
|
|
36
|
+
|
|
37
|
+
// SSR data access: use the ambient @wix/astro server client via @wix/essentials —
|
|
38
|
+
// the SAME pattern the CMS pages use (`auth.elevate(fn)` + a direct module import).
|
|
39
|
+
// Do NOT build a `createClient`/`OAuthStrategy` here keyed off `import.meta.env`:
|
|
40
|
+
// the `*_WIX_CLIENT_ID` env vars are CLIENT-only in @wix/astro and `undefined`
|
|
41
|
+
// during server render, so `createClient`/`OAuthStrategy` throws and the page
|
|
42
|
+
// returns HTTP 500 in production. (It "works" in `astro dev` only because
|
|
43
|
+
// `.env.local` is loaded there — the 500 surfaces only in the deployed runtime.)
|
|
44
|
+
// Client-side islands access the ID differently — see COMPONENTS.md (`astro:env/client`).
|
|
45
|
+
let serviceList: any[] = [];
|
|
46
|
+
try {
|
|
47
|
+
// queryServices uses the QUERY BUILDER, then `.find()` — NOT a
|
|
48
|
+
// `{ query: { filter, paging } }` object. In @wix/bookings the object form
|
|
49
|
+
// returns ZERO items with no error; the builder is what actually queries.
|
|
50
|
+
const result = await auth.elevate(services.queryServices)()
|
|
51
|
+
.limit(100)
|
|
52
|
+
.find();
|
|
53
|
+
// Exclude hidden services from the public listing (filter in JS — robust
|
|
54
|
+
// regardless of whether `hidden` is a queryable builder field).
|
|
55
|
+
serviceList = (result.items ?? []).filter((s: any) => !s.hidden);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error('[services] query failed:', err);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Services V2: slug lives at service.mainSlug.name. ID is service._id (underscore).
|
|
61
|
+
const getSlug = (service: any): string =>
|
|
62
|
+
service.mainSlug?.name ??
|
|
63
|
+
service.supportedSlugs?.[0]?.name ??
|
|
64
|
+
service._id; // fallback to ID if slug is absent
|
|
65
|
+
|
|
66
|
+
// Services V2: price uses `value` (string), not `amount`
|
|
67
|
+
const formatPrice = (service: any): string | undefined => {
|
|
68
|
+
const payment = service.payment;
|
|
69
|
+
if (!payment || payment.rateType === 'NO_FEE') return undefined;
|
|
70
|
+
if (payment.rateType === 'FIXED' && payment.fixed?.price) {
|
|
71
|
+
const { value, currency } = payment.fixed.price;
|
|
72
|
+
if (!value) return undefined;
|
|
73
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency ?? 'USD' }).format(Number(value));
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Duration: lives at service.schedule.availabilityConstraints.sessionDurations[0]
|
|
79
|
+
const getDuration = (service: any): number =>
|
|
80
|
+
service.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? 60;
|
|
81
|
+
---
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Template
|
|
85
|
+
|
|
86
|
+
```astro
|
|
87
|
+
<Layout title="Services">
|
|
88
|
+
<main class="py-4xl">
|
|
89
|
+
<section class="container mx-auto px-lg">
|
|
90
|
+
<header class="mb-3xl">
|
|
91
|
+
<h1 class="font-display text-4xl mb-sm">Our Services</h1>
|
|
92
|
+
<!-- Optional tagline from brand -->
|
|
93
|
+
</header>
|
|
94
|
+
|
|
95
|
+
{serviceList.length === 0 ? (
|
|
96
|
+
<p class="text-secondary">Services coming soon — check back shortly.</p>
|
|
97
|
+
) : (
|
|
98
|
+
<div class="service-grid">
|
|
99
|
+
{serviceList.map((service) => (
|
|
100
|
+
<ServiceCard
|
|
101
|
+
id={service._id}
|
|
102
|
+
slug={getSlug(service)}
|
|
103
|
+
name={service.name ?? 'Untitled'}
|
|
104
|
+
tagLine={service.tagLine}
|
|
105
|
+
durationMinutes={getDuration(service)}
|
|
106
|
+
price={formatPrice(service)}
|
|
107
|
+
imageUrl={service.media?.mainMedia?.image?.url ?? service.media?.items?.[0]?.image?.url}
|
|
108
|
+
type={service.type ?? 'APPOINTMENT'}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</section>
|
|
114
|
+
</main>
|
|
115
|
+
</Layout>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Page 2 — `/services/[slug].astro`
|
|
121
|
+
|
|
122
|
+
### Frontmatter
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
---
|
|
126
|
+
// File is at src/pages/services/[slug].astro
|
|
127
|
+
// ../../ from src/pages/services/ → src/
|
|
128
|
+
import Layout from '../../layouts/Layout.astro';
|
|
129
|
+
import ServiceBookingFlow from '../../components/ServiceBookingFlow';
|
|
130
|
+
import { services } from '@wix/bookings';
|
|
131
|
+
import { auth } from '@wix/essentials';
|
|
132
|
+
|
|
133
|
+
const { slug } = Astro.params;
|
|
134
|
+
|
|
135
|
+
// SSR via @wix/essentials — same ambient pattern as the listing page above and
|
|
136
|
+
// the CMS pages. No createClient/OAuthStrategy, no import.meta.env (those 500 at
|
|
137
|
+
// server render — see the listing-page note).
|
|
138
|
+
let service: any = null;
|
|
139
|
+
try {
|
|
140
|
+
// Query builder + `.find()` (the object form returns 0 items). Fetch the small
|
|
141
|
+
// catalog and match the slug in JS — robust without relying on a nested-field
|
|
142
|
+
// (`mainSlug.name`) builder filter.
|
|
143
|
+
const result = await auth.elevate(services.queryServices)()
|
|
144
|
+
.limit(100)
|
|
145
|
+
.find();
|
|
146
|
+
service =
|
|
147
|
+
(result.items ?? []).find(
|
|
148
|
+
(s: any) => (s.mainSlug?.name ?? s.supportedSlugs?.[0]?.name) === slug,
|
|
149
|
+
) ?? null;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`[services/${slug}] query failed:`, err);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!service) {
|
|
155
|
+
return Astro.redirect('/services', 302);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Services V2: flat fields — `name`, `tagLine`, not `info.name`, `info.tagLine`
|
|
159
|
+
// Price uses `value` (string), not `amount`
|
|
160
|
+
const formatPrice = (payment: any): string | undefined => {
|
|
161
|
+
if (!payment || payment.rateType === 'NO_FEE') return undefined;
|
|
162
|
+
if (payment.rateType === 'FIXED' && payment.fixed?.price) {
|
|
163
|
+
const { value, currency } = payment.fixed.price;
|
|
164
|
+
if (!value) return undefined;
|
|
165
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency ?? 'USD' }).format(Number(value));
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const price = formatPrice(service.payment);
|
|
171
|
+
// Duration: service.schedule.availabilityConstraints.sessionDurations[0]
|
|
172
|
+
const duration = service.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? 60;
|
|
173
|
+
// Media: service.media.mainMedia.image.url (V2 shape)
|
|
174
|
+
const imageUrl = service.media?.mainMedia?.image?.url ?? service.media?.items?.[0]?.image?.url;
|
|
175
|
+
---
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Template
|
|
179
|
+
|
|
180
|
+
```astro
|
|
181
|
+
<!--
|
|
182
|
+
Width: use `container-reading` (max-width: var(--container-prose) + auto margins),
|
|
183
|
+
NOT a bare `max-w-<size>`. In this Tailwind v4 setup any `max-w-<size>` whose key
|
|
184
|
+
the Composer didn't emit in @theme `--container-*` silently falls back to the
|
|
185
|
+
`--spacing-*` scale — e.g. `max-w-4xl` → ~7rem, a ~112px column (one word per line).
|
|
186
|
+
`container-reading` is a project-owned utility that always resolves. See STYLING.md.
|
|
187
|
+
-->
|
|
188
|
+
<Layout title={service.name ?? 'Service'}>
|
|
189
|
+
<main class="py-4xl">
|
|
190
|
+
<div class="container-reading px-lg">
|
|
191
|
+
<!-- Back link -->
|
|
192
|
+
<a href="/services" class="text-secondary hover:text-primary text-sm mb-xl block">
|
|
193
|
+
← All Services
|
|
194
|
+
</a>
|
|
195
|
+
|
|
196
|
+
<!-- Optional banner image — render ONLY when the service has media. The
|
|
197
|
+
default seed is text-only, so most services have no image; rendering a
|
|
198
|
+
fixed-ratio placeholder there collapses to a thin bar and crowds the
|
|
199
|
+
text. Single full-width column when image-less. -->
|
|
200
|
+
{imageUrl && (
|
|
201
|
+
<img src={imageUrl} alt={service.name} class="w-full h-64 object-cover rounded-lg mb-xl" />
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
<!-- Service hero — single column -->
|
|
205
|
+
<div class="flex flex-col gap-md mb-3xl">
|
|
206
|
+
{/* V2 flat fields: name, tagLine, description (not info.name etc.) */}
|
|
207
|
+
<h1 class="font-display text-4xl">{service.name}</h1>
|
|
208
|
+
{service.tagLine && (
|
|
209
|
+
<p class="text-secondary text-lg">{service.tagLine}</p>
|
|
210
|
+
)}
|
|
211
|
+
<div class="flex gap-md items-center text-sm">
|
|
212
|
+
<span class="font-medium">{duration} min</span>
|
|
213
|
+
{price && <span class="text-primary font-bold text-lg">{price}</span>}
|
|
214
|
+
</div>
|
|
215
|
+
{service.description && (
|
|
216
|
+
<p class="text-base leading-relaxed">{service.description}</p>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<!-- Booking flow — client-side islands -->
|
|
221
|
+
<section class="border-t border-[--color-border] pt-2xl">
|
|
222
|
+
<h2 class="font-display text-2xl mb-xl">Book This Service</h2>
|
|
223
|
+
<ServiceBookingFlow
|
|
224
|
+
serviceId={service._id}
|
|
225
|
+
serviceName={service.name ?? 'this service'}
|
|
226
|
+
serviceType={service.type ?? 'APPOINTMENT'}
|
|
227
|
+
client:only="react"
|
|
228
|
+
/>
|
|
229
|
+
</section>
|
|
230
|
+
</div>
|
|
231
|
+
</main>
|
|
232
|
+
</Layout>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### ServiceBookingFlow coordinator
|
|
236
|
+
|
|
237
|
+
Because `AvailabilityCalendar` and `BookingForm` share state (the selected slot) and need to transition between each other, write a thin coordinator island `ServiceBookingFlow.tsx` that manages this state:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// src/components/ServiceBookingFlow.tsx
|
|
241
|
+
import { useState } from 'react';
|
|
242
|
+
import AvailabilityCalendar from './AvailabilityCalendar';
|
|
243
|
+
import BookingForm from './BookingForm';
|
|
244
|
+
|
|
245
|
+
interface Props {
|
|
246
|
+
serviceId: string;
|
|
247
|
+
serviceName: string;
|
|
248
|
+
serviceType: 'APPOINTMENT' | 'CLASS'; // threaded into both islands
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default function ServiceBookingFlow({ serviceId, serviceName, serviceType }: Props) {
|
|
252
|
+
const [selectedSlot, setSelectedSlot] = useState<any>(null);
|
|
253
|
+
|
|
254
|
+
const handleSuccess = (bookingId: string, startDate?: string) => {
|
|
255
|
+
const params = new URLSearchParams({ bookingId });
|
|
256
|
+
if (startDate) params.set('startDate', startDate);
|
|
257
|
+
if (serviceName) params.set('service', serviceName);
|
|
258
|
+
window.location.href = `/booking-confirmation?${params.toString()}`;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (!selectedSlot) {
|
|
262
|
+
return (
|
|
263
|
+
<AvailabilityCalendar
|
|
264
|
+
serviceId={serviceId}
|
|
265
|
+
serviceName={serviceName}
|
|
266
|
+
serviceType={serviceType}
|
|
267
|
+
onSlotSelected={setSelectedSlot}
|
|
268
|
+
/>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<BookingForm
|
|
274
|
+
serviceId={serviceId}
|
|
275
|
+
serviceName={serviceName}
|
|
276
|
+
serviceType={serviceType}
|
|
277
|
+
slot={selectedSlot}
|
|
278
|
+
onSuccess={handleSuccess}
|
|
279
|
+
onCancel={() => setSelectedSlot(null)}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Add `ServiceBookingFlow` to the files you own and include it in your pre-return assertion.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Page 3 — `/booking-confirmation.astro`
|
|
290
|
+
|
|
291
|
+
### Behavior — show the real status + cancel inline
|
|
292
|
+
|
|
293
|
+
The confirmation page **re-fetches the booking** to show its **actual status** (`CONFIRMED` vs `PENDING` approval) — so the user sees whether the booking was approved, not just an optimistic "you're booked" — and embeds the cancel action. It re-uses the **anonymous-action-token** flow: the server elevates (app identity) to mint a per-booking token (`getAnonymousActionToken`), then reads it with the no-auth `bookingsGetBookingAnonymously`, falling back to the redirect params (`?bookingId=...&startDate=...&service=...`) if the lookup fails. Cancel is the same `<ManageBooking>` island used by `/manage-booking` (pass `showSummary={false}` to avoid duplicating the headline). See the canonical template `booking-confirmation.astro`.
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// frontmatter (essentials):
|
|
297
|
+
const bookingId = Astro.url.searchParams.get('bookingId');
|
|
298
|
+
const { token } = bookingId ? await auth.elevate(bookings.getAnonymousActionToken)(bookingId) : { token: null };
|
|
299
|
+
const { booking, allowedAnonymousActions } = token ? await bookings.bookingsGetBookingAnonymously(token) : {};
|
|
300
|
+
const status = booking?.status ?? null; // CONFIRMED | PENDING | …
|
|
301
|
+
const isPending = status === 'PENDING' || status === 'PENDING_APPROVAL';
|
|
302
|
+
// Render: "You're booked / confirmed" vs "Booking received — awaiting confirmation" by status,
|
|
303
|
+
// a Status badge, and <ManageBooking … canCancel={!!allowedAnonymousActions?.cancel} showSummary={false} />.
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
> **Two different `elevate`s, don't conflate them:** `@wix/essentials` `auth.elevate(fn)` works in `@wix/astro` SSR (used here to mint the token and across the listing/detail/CMS pages); the unavailable one is `wixClient.auth.elevate` (the instance method on a hand-built `createClient({ auth: OAuthStrategy(...) })`).
|
|
307
|
+
|
|
308
|
+
### Template
|
|
309
|
+
|
|
310
|
+
Use the canonical template verbatim and adapt copy/styling: `<SKILL_ROOT>/references/astro/templates/bookings/booking-confirmation.astro`. It renders a status-aware headline ("You're booked / confirmed" vs "Booking received — awaiting confirmation"), a Status badge, and the embedded `<ManageBooking … showSummary={false} />` cancel — wrapped in `max-w-prose` (a built-in literal; never a bare `max-w-<size>`, which can fall through to the spacing scale).
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Server endpoints + manage-booking page (verified live)
|
|
315
|
+
|
|
316
|
+
Some bookings operations require **Manage Bookings** scope and so **cannot** run from the browser `OAuthStrategy` client — they must be Astro **API routes** (`output: "server"`) that elevate to app identity. Elevation rule (important): `auth.elevate()` only grants permissions for a real **SDK function**; for raw REST with no SDK wrapper, elevate the authenticated fetch itself.
|
|
317
|
+
|
|
318
|
+
### `src/pages/api/confirm-booking.ts` — hold the seat (always, for create flows)
|
|
319
|
+
`createBooking` returns `CREATED`, which holds nothing. Confirm to occupy the seat + validate availability:
|
|
320
|
+
```typescript
|
|
321
|
+
import { auth } from "@wix/essentials";
|
|
322
|
+
import { bookings } from "@wix/bookings";
|
|
323
|
+
// POST { bookingId, revision }
|
|
324
|
+
await auth.elevate(bookings.confirmBooking)(bookingId, String(revision), {
|
|
325
|
+
paymentStatus: "NOT_PAID",
|
|
326
|
+
flowControlSettings: { checkAvailabilityValidation: true },
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### `src/pages/api/waitlist.ts` — native waitlist (v1; no v2/v3 exists)
|
|
331
|
+
Register on the native waitlist when a session is full. Resolve a `contactId` (Contacts API, by email → create if missing), then `POST /bookings/v1/waitlist/register`. All via the **elevated authenticated fetch** (raw REST, so elevate `fetchWithAuth`, not a closure):
|
|
332
|
+
```typescript
|
|
333
|
+
import { auth, httpClient } from "@wix/essentials";
|
|
334
|
+
const ef = auth.elevate(httpClient.fetchWithAuth as typeof fetch) as typeof fetch;
|
|
335
|
+
const api = (url: string, body: unknown) =>
|
|
336
|
+
ef(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
337
|
+
|
|
338
|
+
// 1. contactId — query then create
|
|
339
|
+
let contactId = (await (await api("https://www.wixapis.com/contacts/v4/contacts/query",
|
|
340
|
+
{ query: { filter: { "info.emails.email": email } } })).json()).contacts?.[0]?.id;
|
|
341
|
+
if (!contactId) contactId = (await (await api("https://www.wixapis.com/contacts/v4/contacts",
|
|
342
|
+
{ info: { name: { first }, emails: { items: [{ email, primary: true }] } } })).json()).contact?.id;
|
|
343
|
+
|
|
344
|
+
// 2. register — waitingResource is the v1 session id from the SESSION_CAPACITY_EXCEEDED message
|
|
345
|
+
await api("https://www.wixapis.com/bookings/v1/waitlist/register", {
|
|
346
|
+
waitingResource: sessionId,
|
|
347
|
+
formInfo: {
|
|
348
|
+
paymentSelection: [{ rateLabel: "general", numberOfParticipants: totalParticipants }],
|
|
349
|
+
contactDetails: { contactId, firstName: first, email },
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### `src/pages/manage-booking.astro` + `ManageBooking.tsx` — view/cancel (anonymous-token flow)
|
|
355
|
+
A plain visitor token can't list/cancel bookings (`queryExtendedBookings`/`cancelBooking` are APP/MEMBER only). The supported visitor path is the **anonymous action token**: the page (SSR) elevates to **mint** a per-booking token, then the no-auth anonymous methods act on it (the token can safely go to the browser):
|
|
356
|
+
```typescript
|
|
357
|
+
// manage-booking.astro frontmatter — bookingId from ?bookingId=
|
|
358
|
+
const { token } = await auth.elevate(bookings.getAnonymousActionToken)(bookingId); // APP identity → elevate
|
|
359
|
+
const { booking, allowedAnonymousActions } = await bookings.bookingsGetBookingAnonymously(token); // no auth
|
|
360
|
+
// Pass token + booking.revision + allowedAnonymousActions.cancel to a <ManageBooking client:only> island.
|
|
361
|
+
```
|
|
362
|
+
In the island (no-auth, visitor client OK):
|
|
363
|
+
```typescript
|
|
364
|
+
await wixClient.bookings.bookingsCancelBookingAnonymously(token, revision); // cancel
|
|
365
|
+
```
|
|
366
|
+
Reschedule-in-place uses `bookingsRescheduleBookingAnonymously(token, slot, { revision })`, but the CLASS `V2Slot` shape isn't fully pinned — prefer **cancel-and-rebook** (link to the service) unless you've verified the class slot shape live.
|
|
367
|
+
|
|
368
|
+
Link to it from the confirmation page: `/manage-booking?bookingId=<id>&service=<name>`.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Navigation contribution
|
|
373
|
+
|
|
374
|
+
After writing your three route files, patch `src/layouts/Navigation.astro` to add the Services nav link. Insert at the `<!-- nav:links -->` marker:
|
|
375
|
+
|
|
376
|
+
```astro
|
|
377
|
+
<!-- nav:links -->
|
|
378
|
+
<a href="/services">Services</a>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Follow the marker discipline rules in `references/shared/IMPLEMENTER.md` — preserve the marker line, insert after it, do not disturb adjacent content.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Home page contribution
|
|
386
|
+
|
|
387
|
+
Patch `src/pages/index.astro` to add a brief services teaser section. Insert at the `<!-- home:bookings -->` marker (emitted by `compose.mjs` when bookings is loaded). You are a **shell-patcher** (BUILD-astro § build wave, chain B) — `index.astro`/`Navigation.astro` patches are serialized against the other shell-patching verticals. The teaser should:
|
|
388
|
+
- Show the first 3 services (query the live SDK like `/services` does; your `seeded.bookings` slice tells you what exists)
|
|
389
|
+
- Include a "View All Services" CTA linking to `/services`
|
|
390
|
+
- Use `<ServiceCard>` for each service entry
|
|
391
|
+
|
|
392
|
+
If the `<!-- home:bookings -->` marker is absent from the file, skip the home contribution — do NOT invent a new insertion point.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Pre-return file-existence assertion
|
|
397
|
+
|
|
398
|
+
Before returning `status: "complete"`, verify all of these files exist on disk:
|
|
399
|
+
- `src/pages/services/index.astro`
|
|
400
|
+
- `src/pages/services/[slug].astro`
|
|
401
|
+
- `src/pages/booking-confirmation.astro`
|
|
402
|
+
- `src/pages/manage-booking.astro`
|
|
403
|
+
- `src/components/ServiceBookingFlow.tsx`
|
|
404
|
+
|
|
405
|
+
Missing file → return `status: "partial"` with `errors: [{ code: "PHASE4_FILE_MISSING", path: "<path>" }]`.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Return contract
|
|
410
|
+
|
|
411
|
+
```json
|
|
412
|
+
{
|
|
413
|
+
"status": "complete",
|
|
414
|
+
"phase": "pages",
|
|
415
|
+
"vertical": "bookings",
|
|
416
|
+
"files": [
|
|
417
|
+
"src/pages/services/index.astro",
|
|
418
|
+
"src/pages/services/[slug].astro",
|
|
419
|
+
"src/pages/booking-confirmation.astro",
|
|
420
|
+
"src/pages/manage-booking.astro",
|
|
421
|
+
"src/components/ServiceBookingFlow.tsx"
|
|
422
|
+
],
|
|
423
|
+
"contributions": [
|
|
424
|
+
{ "file": "src/layouts/Navigation.astro", "marker": "nav:links" },
|
|
425
|
+
{ "file": "src/pages/index.astro", "marker": "home:bookings" }
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
```
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# CMS Foundations — Code Patterns for `@wix/data` (astro)
|
|
2
|
+
|
|
3
|
+
The **rendering / code-patterns** half of CMS foundations: service module template, `@wix/data` SDK queries, image resolution, elevated access, and rich-text (Ricos) rendering — everything a CMS **page** scope needs to read collections and render them in Astro.
|
|
4
|
+
|
|
5
|
+
> **Seeding lives in the business track.** Creating collections and inserting/verifying items via REST is **not** here — it is in `<SKILL_ROOT>/references/cms/CMS_FOUNDATIONS.md` (the `seed` scope's doc). This file is consumed by the `pages` scope; that file by the `seed` scope. The `cms/INSTRUCTIONS.md` router points each scope at the right half.
|
|
6
|
+
|
|
7
|
+
> **Import note (read first).** The Wix Data items API is the **`items`** named export of **`@wix/data`**: `import { items } from "@wix/data"`, then `items.query` / `items.insert` / `items.update` / `items.remove` (plus `items.bulkInsert` / `bulkSave` / `bulkPatch` / …). This is the documented form and what every CMS page here uses. Do **not** import from the internal `@wix/wix-data-items-sdk`. SETUP.md's per-pack install table (Step 4c) installs `@wix/data` + `@wix/essentials` for the cms pack.
|
|
8
|
+
|
|
9
|
+
## Rendering Ricos rich-text fields
|
|
10
|
+
|
|
11
|
+
Wix CMS stores `RICH_TEXT` / `RICH_CONTENT` fields as **Ricos JSON** — a structured node tree (PARAGRAPH, HEADING, BULLETED_LIST, etc.), not HTML. The dashboard's rich-text editor reads and writes this format.
|
|
12
|
+
|
|
13
|
+
For SSR Astro pages (about, faq, portfolio, team, resource), import the seeded `renderRicos` util from `src/utils/ricos.ts`. The util is shipped by `seed-utilities.sh` (copied from `<SKILL_ROOT>/shared-utilities/ricos.ts` during Setup), so it's already on disk — do NOT inline a Ricos walker in your page.
|
|
14
|
+
|
|
15
|
+
```astro
|
|
16
|
+
---
|
|
17
|
+
import { items } from "@wix/data";
|
|
18
|
+
import { auth } from "@wix/essentials";
|
|
19
|
+
import { renderRicos } from "../utils/ricos";
|
|
20
|
+
|
|
21
|
+
const elevatedQuery = auth.elevate(items.query);
|
|
22
|
+
const { items: results } = await elevatedQuery("about-content")
|
|
23
|
+
.limit(1)
|
|
24
|
+
.find();
|
|
25
|
+
const about = results[0];
|
|
26
|
+
const bodyHtml = renderRicos(about?.body);
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<article class="prose">
|
|
30
|
+
<h1>{about?.heading}</h1>
|
|
31
|
+
<div set:html={bodyHtml} />
|
|
32
|
+
</article>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The util covers the common subset (PARAGRAPH, HEADING 1-6, BULLETED_LIST / ORDERED_LIST / LIST_ITEM, BLOCKQUOTE, DIVIDER + BOLD / ITALIC / UNDERLINE / LINK decorations). Anything outside that set renders defensively as a `<p>` with the raw text — never throws.
|
|
36
|
+
|
|
37
|
+
> **Why not @wix/ricos?** The blog vertical uses `@wix/ricos`'s React `RicosViewer` as a client island because blog posts can carry the full Ricos feature set (galleries, polls, embedded media). For static CMS pages, that's ~80kb of React for paragraphs and lists — overkill. Use the seeded SSR walker instead.
|
|
38
|
+
|
|
39
|
+
> **Do NOT `set:html={item.body}` directly.** That ships JSON-encoded text into the page (e.g. `[object Object]` or `{"nodes":...}`). Always go through `renderRicos`.
|
|
40
|
+
|
|
41
|
+
> **Critical Rules — Read Before Starting**
|
|
42
|
+
> 1. **Collection IDs have NO namespace** — user collections use just the name (e.g., `"Projects"`). Only Wix App collections use `<namespace>/<name>`. Verify via `curl`: `GET /wix-data/v2/collections?fields=id`.
|
|
43
|
+
> 2. **SDK query pattern** — `items.query("CollectionId").find()`. There is no `items.queryDataItems()` in the SDK.
|
|
44
|
+
> 3. **`auth.elevate` on every query** — without it, restricted collections silently return no items.
|
|
45
|
+
> 4. **CMS only for user collections** — use `@wix/blog` for blog posts, `@wix/stores` for products, `@wix/forms` for form submissions.
|
|
46
|
+
|
|
47
|
+
## Prerequisites (page-side)
|
|
48
|
+
|
|
49
|
+
- `@wix/data` + `@wix/essentials` installed (collected by SETUP.md Step 4c — do NOT install independently).
|
|
50
|
+
- The collections + items already created and seeded by the `seed` scope (see `../../cms/CMS_FOUNDATIONS.md`). Pages only **read**.
|
|
51
|
+
|
|
52
|
+
## Collection ID Format
|
|
53
|
+
|
|
54
|
+
Native (user-created) CMS collections use **just the collection name** — no namespace prefix.
|
|
55
|
+
|
|
56
|
+
Example: if the collection is called `Projects`, the collection ID is simply `"Projects"`.
|
|
57
|
+
|
|
58
|
+
Only Wix App collections (e.g., `Members/PublicData`, `Locations/Locations`) use a `<namespace>/<name>` format. User-created collections never use a namespace.
|
|
59
|
+
|
|
60
|
+
> **How to verify**: Call `GET /wix-data/v2/collections?fields=id` via `curl` (with the standard REST headers from `../../shared/AUTHENTICATION.md`) and use the exact `id` value from the response. Never guess — the dashboard name may differ from the ID.
|
|
61
|
+
|
|
62
|
+
## Quick Reference — Inline Query (Simple Pages)
|
|
63
|
+
|
|
64
|
+
For pages that just need to list items from a collection:
|
|
65
|
+
|
|
66
|
+
```astro
|
|
67
|
+
---
|
|
68
|
+
import { items } from "@wix/data";
|
|
69
|
+
import { auth } from "@wix/essentials";
|
|
70
|
+
|
|
71
|
+
// Always elevate queries for permission safety
|
|
72
|
+
const elevatedQuery = auth.elevate(items.query);
|
|
73
|
+
const { items: results } = await elevatedQuery("MyCollection")
|
|
74
|
+
.ascending("sortField")
|
|
75
|
+
.limit(50)
|
|
76
|
+
.find();
|
|
77
|
+
|
|
78
|
+
// Fields are directly on each item (NOT item.data.field)
|
|
79
|
+
const myItems = results.map((item) => ({
|
|
80
|
+
name: item.name, // correct
|
|
81
|
+
// name: item.data.name // wrong — REST pattern, not SDK
|
|
82
|
+
}));
|
|
83
|
+
---
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> **Common mistake:** `items.queryDataItems(...)` does NOT exist in the
|
|
87
|
+
> `@wix/data` SDK. The REST API uses `/items/query` with a `dataCollectionId`
|
|
88
|
+
> body field, but the SDK uses `items.query("collectionId")` — a chainable
|
|
89
|
+
> QueryBuilder. Do not confuse REST and SDK patterns.
|
|
90
|
+
|
|
91
|
+
## Service Module Template
|
|
92
|
+
|
|
93
|
+
Every CMS use case follows the same `src/lib/{usecase}.ts` pattern — a typed interface and query functions that pages import.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { items } from "@wix/data";
|
|
97
|
+
import { auth } from "@wix/essentials";
|
|
98
|
+
import { media } from "@wix/sdk";
|
|
99
|
+
|
|
100
|
+
// -- Collection ID (use exact name from Wix dashboard — no namespace for native collections) --
|
|
101
|
+
const COLLECTION_ID = "collection-name";
|
|
102
|
+
|
|
103
|
+
// -- Typed interface matching collection fields --
|
|
104
|
+
export interface CollectionItem {
|
|
105
|
+
_id: string;
|
|
106
|
+
title: string;
|
|
107
|
+
slug: string;
|
|
108
|
+
// ...add fields matching your collection schema
|
|
109
|
+
coverImage?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -- Image resolution helper --
|
|
113
|
+
function resolveImage(wixImageUrl: string | undefined, width = 800, height = 600): string | undefined {
|
|
114
|
+
if (!wixImageUrl) return undefined;
|
|
115
|
+
return media.getScaledToFillImageUrl(wixImageUrl, width, height, {});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// -- Query all items --
|
|
119
|
+
// Wrap every Wix SDK await in try/catch. If the query throws at SSR, an empty
|
|
120
|
+
// array keeps the page renderable instead of crashing it. (Unguarded SSR
|
|
121
|
+
// awaits truncate Astro's response stream mid-body — nav renders, then blank.)
|
|
122
|
+
export async function queryItems(): Promise<CollectionItem[]> {
|
|
123
|
+
try {
|
|
124
|
+
const elevatedQuery = auth.elevate(items.query);
|
|
125
|
+
const { items: results } = await elevatedQuery(COLLECTION_ID)
|
|
126
|
+
.ascending("orderIndex")
|
|
127
|
+
.limit(50)
|
|
128
|
+
.find();
|
|
129
|
+
|
|
130
|
+
return results.map((item) => ({
|
|
131
|
+
...item,
|
|
132
|
+
coverImage: resolveImage(item.coverImage),
|
|
133
|
+
})) as CollectionItem[];
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`[cms:${COLLECTION_ID}] query failed:`, err);
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// -- Get single item by slug --
|
|
141
|
+
export async function getItemBySlug(slug: string): Promise<CollectionItem | null> {
|
|
142
|
+
try {
|
|
143
|
+
const elevatedQuery = auth.elevate(items.query);
|
|
144
|
+
const { items: results } = await elevatedQuery(COLLECTION_ID)
|
|
145
|
+
.eq("slug", slug)
|
|
146
|
+
.limit(1)
|
|
147
|
+
.find();
|
|
148
|
+
|
|
149
|
+
const item = results[0];
|
|
150
|
+
if (!item) return null;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
...item,
|
|
154
|
+
coverImage: resolveImage(item.coverImage),
|
|
155
|
+
} as CollectionItem;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(`[cms:${COLLECTION_ID}] getItemBySlug failed:`, err);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Key details:
|
|
164
|
+
- `auth.elevate(items.query)` wraps the query for restricted collections — always use this by default since collection permissions vary
|
|
165
|
+
- `media.getScaledToFillImageUrl(url, w, h, {})` resolves `wix:image://` URLs to sized CDN URLs
|
|
166
|
+
- Sort by `orderIndex` (ascending) for manually ordered content, or `_createdDate` (descending) for chronological
|
|
167
|
+
- The interface must match the collection fields — check the dashboard, don't guess
|
|
168
|
+
|
|
169
|
+
## Multi-Image Resolution
|
|
170
|
+
|
|
171
|
+
For collections with image arrays (e.g., gallery images):
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
function resolveImages(imageUrls: string[] | undefined, width = 800, height = 600): string[] {
|
|
175
|
+
if (!imageUrls || imageUrls.length === 0) return [];
|
|
176
|
+
return imageUrls
|
|
177
|
+
.map((url) => media.getScaledToFillImageUrl(url, width, height, {}))
|
|
178
|
+
.filter(Boolean);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Category Filtering Pattern
|
|
183
|
+
|
|
184
|
+
Many use cases filter by category via URL search params (server-rendered, no client JS):
|
|
185
|
+
|
|
186
|
+
```astro
|
|
187
|
+
---
|
|
188
|
+
const activeCategory = Astro.url.searchParams.get("category");
|
|
189
|
+
const allItems = await queryItems();
|
|
190
|
+
const filtered = activeCategory
|
|
191
|
+
? allItems.filter((item) => item.category === activeCategory)
|
|
192
|
+
: allItems;
|
|
193
|
+
const categories = [...new Set(allItems.map((item) => item.category).filter(Boolean))];
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
<nav>
|
|
197
|
+
<a href="?" class:list={[!activeCategory && "active"]}>All</a>
|
|
198
|
+
{categories.map((cat) => (
|
|
199
|
+
<a
|
|
200
|
+
href={`?category=${encodeURIComponent(cat)}`}
|
|
201
|
+
class:list={[activeCategory === cat && "active"]}
|
|
202
|
+
>{cat}</a>
|
|
203
|
+
))}
|
|
204
|
+
</nav>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
> **Styling note:** Category filter pill styling composes `@theme` tokens as Tailwind utilities at the call site — see `../../shared/STYLING.md`.
|
|
208
|
+
|
|
209
|
+
## Testing
|
|
210
|
+
|
|
211
|
+
1. Confirm the collection exists in the Wix dashboard → CMS (the `seed` scope created it — see `../../cms/CMS_FOUNDATIONS.md`).
|
|
212
|
+
2. Run `npx @wix/cli@latest dev`.
|
|
213
|
+
3. Navigate to the listing page — should show items.
|
|
214
|
+
4. Click an item — should navigate to the detail page.
|