@wix/agent-skills 1.19.1 → 1.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +1 -1
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +7 -2
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +46 -22
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +17 -4
- package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +71 -31
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +3 -4
- package/skills/wix-base44-connector/SKILL.md +46 -24
- package/skills/wix-headless/SKILL.md +1 -0
- package/skills/wix-headless/references/CAPABILITIES.md +7 -2
- package/skills/wix-headless/references/SDK_HANDOFF.md +2 -1
- package/skills/wix-headless/references/inline-recipes/experience-store.md +48 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +350 -114
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +1 -1
- package/skills/wix-headless/references/managed/CREATE.md +5 -0
- package/skills/wix-manage/SKILL.md +4 -4
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +4 -1
- package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -1
- package/skills/wix-manage/references/bookings/create-class-service.md +3 -1
- package/skills/wix-manage/references/bookings/create-course-service.md +4 -2
- package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +68 -3
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-seasonal-promotion.md +1 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +108 -14
- package/skills/wix-manage/references/seo/manage-content-plan.md +1 -1
- package/skills/wix-manage/references/sites/upload-static-site.md +264 -34
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +68 -26
- package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +32 -14
- package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +14 -3
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +41 -35
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +77 -30
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +31 -2
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +31 -6
|
@@ -1,53 +1,283 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "Upload a Website or HTML Files"
|
|
3
|
-
description:
|
|
3
|
+
description: Publish a user's ready-made website — an index.html, a static build, or a zip exported from an AI builder or any other tool — as a new live Wix site. Covers both ways to get there — handing the user the Wix Headless drop page, and calling the Wix Headless instant-site REST API yourself when you can reach the files and make outbound HTTPS requests. When you also hold the user's identity, covers putting the published site straight into their Wix account and reading back its final live URL. Use whenever the user wants to upload, publish, deploy, or host their own HTML/CSS/JS as a NEW site, including files generated for them earlier in the conversation, or to update a site already published this way (iterate on the same site instead of creating another). Not for migrating a live store/site from another platform by URL or from CSV exports (use Site Import), not for adding HTML or custom code into an existing Wix site, and not for uploading images or documents to a site's media files.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Upload a Website or HTML Files
|
|
7
7
|
|
|
8
|
-
The user
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
explain the flow:
|
|
8
|
+
The user has a finished website as files — hand-written HTML, a static build, a
|
|
9
|
+
zip, or the output of an AI site builder — and wants it live on Wix as a new
|
|
10
|
+
site. There are two ways to get there.
|
|
12
11
|
|
|
12
|
+
## The two paths
|
|
13
|
+
|
|
14
|
+
**[Path A — hand the user the drop page](#path-a--hand-the-user-the-drop-page).**
|
|
15
|
+
One URL; the user uploads in the browser and gets a live site. This works for
|
|
16
|
+
**any** agent and any user — it's the default. Reach for it whenever you can't
|
|
17
|
+
clearly do Path B.
|
|
18
|
+
|
|
19
|
+
**[Path B — publish the files yourself](#path-b--publish-the-files-yourself).**
|
|
20
|
+
Three calls put the site live without the user touching a browser. Available only
|
|
21
|
+
when **both** hold: you can read the file bytes (you generated them here, or
|
|
22
|
+
they're reachable from where you run), **and** you can do a `multipart/form-data`
|
|
23
|
+
file upload from a shell (`curl` or equivalent). Reaching APIs *only* through the
|
|
24
|
+
Wix API-call/execute-API tools does **not** count — they can't upload a local
|
|
25
|
+
file.
|
|
26
|
+
|
|
27
|
+
Prefer Path B when you can genuinely do it — the user gets a live site without
|
|
28
|
+
uploading anything themselves. Otherwise, and by default, take Path A. Decide
|
|
29
|
+
honestly: if you can't read the bytes or can't upload from a shell, hand over the
|
|
30
|
+
drop page rather than attempting Path B and reporting an upload you couldn't
|
|
31
|
+
perform.
|
|
32
|
+
|
|
33
|
+
## Path A — hand the user the drop page
|
|
34
|
+
|
|
35
|
+
Send the user here:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
https://www.wix.com/headless/drop?utm_campaign=mcp&agent=<your-agent-id>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Keep `utm_campaign=mcp`** (the drop-flow referral tag — don't change it) **and set
|
|
42
|
+
`agent` to your own identifier** — the coding agent or tool you are (e.g.
|
|
43
|
+
`claude-code`, `cursor`, `codex-cli`, `windsurf`, `github-copilot`, or your product's
|
|
44
|
+
name). Hand the URL with `agent` filled in and both params intact. There the user drags
|
|
45
|
+
in their files (no login), Wix hosts them immediately on a live URL, and a banner
|
|
46
|
+
offers to sign in and keep the site. Tell them the requirements from
|
|
47
|
+
[What the upload accepts](#what-the-upload-accepts-and-how-it-fails) so it doesn't
|
|
48
|
+
fail on the first try.
|
|
49
|
+
|
|
50
|
+
## Path B — publish the files yourself
|
|
51
|
+
|
|
52
|
+
> **Run every call from your shell with `curl` — not the Wix API-call/execute-API
|
|
53
|
+
> tools.** The upload (step 2) and the download in
|
|
54
|
+
> [Add a backend](#keep-building-add-a-backend-when-you-need-one) move a **file**
|
|
55
|
+
> to or from disk; those tools proxy JSON and can't (hand-building a multipart
|
|
56
|
+
> body inline silently drops every image and font). Create and release are plain
|
|
57
|
+
> JSON and would work through anything, but the upload needs a real file client
|
|
58
|
+
> anyway, so keep it all in the shell. **None of steps 1–3 (or the download) need
|
|
59
|
+
> auth — they're anonymous.** Only [claim](#4-put-the-site-in-the-users-account)
|
|
60
|
+
> needs the user's identity.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Base URL: https://www.wixapis.com/headless-business-setup
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Generate `anonymousId` yourself — any UUID, **once per site, not once per
|
|
67
|
+
request** — and reuse it (with the returned `metaSiteId`) for every call for that
|
|
68
|
+
site, including later changes ([Iterate](#iterate-while-anonymous-dont-create-a-new-site)).
|
|
69
|
+
**Finish within one hour of step 1**: after that the record expires and every
|
|
70
|
+
later step, claim included, returns `404`.
|
|
71
|
+
|
|
72
|
+
### 1. Create the site
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -sS -X POST \
|
|
76
|
+
"https://www.wixapis.com/headless-business-setup/v1/headless-business/anonymous/$ANONYMOUS_ID"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "metaSiteId": "f0ad8672-09e9-4111-b6fe-070c70bd2df5",
|
|
81
|
+
"projectId": "54528d34-b23c-4fbe-b07d-2c522350abd7" }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Keep both: `metaSiteId` addresses the site in every later call; `projectId`
|
|
85
|
+
builds the save link in step 3b.
|
|
86
|
+
|
|
87
|
+
### 2. Upload the files
|
|
88
|
+
|
|
89
|
+
One `multipart/form-data` request carrying every file. Each file is a part named
|
|
90
|
+
`files` whose **filename is its path relative to the site root** — that is how
|
|
91
|
+
subdirectories survive; with `curl`, set it explicitly with `;filename=` whenever
|
|
92
|
+
it isn't just the basename. A single `.zip` part works too — send it alone and
|
|
93
|
+
it's unpacked server-side, a single wrapping folder stripped.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
AGENT=claude-code # ← replace with your own identifier (see note below)
|
|
97
|
+
curl -sS -X POST \
|
|
98
|
+
"https://www.wixapis.com/headless-business-setup/v1/headless-business/anonymous/$ANONYMOUS_ID/$META_SITE_ID/upload?campaign=mcp&agent=$AGENT" \
|
|
99
|
+
-F "files=@index.html;filename=index.html" \
|
|
100
|
+
-F "files=@assets/styles.css;filename=assets/styles.css" \
|
|
101
|
+
-F "files=@assets/logo.png;filename=assets/logo.png"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{ "uploadId": "03244542-d820-42f6-acfa-166c6658b1a6" }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Keep `campaign=mcp` (the drop-flow referral tag — don't change it) and set
|
|
109
|
+
`agent=$AGENT` to **your own identifier**: the coding agent or tool performing this
|
|
110
|
+
drop, so the upload is attributed to the real client. Use a short, stable,
|
|
111
|
+
lowercase-hyphenated slug — e.g. `claude-code`, `cursor`, `codex-cli`, `windsurf`,
|
|
112
|
+
`github-copilot`, or your product's name; if you genuinely can't name yourself, use
|
|
113
|
+
`unknown-agent`. Send the **same** `agent` on every upload for this site, matching
|
|
114
|
+
the `agent` you'd use in Path A. Nothing is live yet; this only stages and validates.
|
|
115
|
+
|
|
116
|
+
### 3. Release — the site goes live
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
curl -sS -X POST \
|
|
120
|
+
"https://www.wixapis.com/headless-business-setup/v1/headless-business/anonymous/$ANONYMOUS_ID/$META_SITE_ID/release" \
|
|
121
|
+
-H 'Content-Type: application/json' \
|
|
122
|
+
-d "{\"uploadId\":\"$UPLOAD_ID\"}"
|
|
13
123
|
```
|
|
14
|
-
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{ "siteUrl": "https://instant-hguwrvtcrniw-headlessstack-140d.wix-site-host.com" }
|
|
15
127
|
```
|
|
16
128
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
129
|
+
The site is live at `siteUrl` immediately.
|
|
130
|
+
|
|
131
|
+
#### Iterate while anonymous (don't create a new site)
|
|
132
|
+
|
|
133
|
+
Simple static changes — new copy, another page, a different look — happen here,
|
|
134
|
+
**while the site is still anonymous**, and this is the only window for them:
|
|
135
|
+
re-run steps 2–3 with the **same `anonymousId` and `metaSiteId`**; the upload
|
|
136
|
+
replaces the site's contents and `siteUrl` stays the same. Send the full file set
|
|
137
|
+
each time (release replaces, it doesn't merge). Don't go back to step 1 — a fresh
|
|
138
|
+
site per change leaves the user a trail of abandoned sites on changing URLs; only
|
|
139
|
+
create another for a genuinely separate site.
|
|
140
|
+
|
|
141
|
+
So **iterate first, claim last**: refine on the same ids until the site is right,
|
|
142
|
+
*then* finish. Once claimed, these anonymous endpoints stop working and the only
|
|
143
|
+
way to keep changing the site in code is a headless project
|
|
144
|
+
([Keep building](#keep-building-add-a-backend-when-you-need-one)).
|
|
145
|
+
|
|
146
|
+
**When the site is final, finish based on identity:**
|
|
147
|
+
|
|
148
|
+
- **You have the user's access token** — claim it into their account (step 4).
|
|
149
|
+
Don't stop at a temporary site: an unclaimed site expires, so saving it is part
|
|
150
|
+
of publishing, not an extra favour to ask about.
|
|
151
|
+
- **You have no user identity** — stop here; this is a finished, legitimate result
|
|
152
|
+
(exactly what the drop page produces for every visitor). Give the user `siteUrl`
|
|
153
|
+
plus the save link, which is how the site survives:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
https://www.wix.com/live-headless-site/{projectId}?anonymousId={anonymousId}
|
|
157
|
+
```
|
|
20
158
|
|
|
21
|
-
|
|
159
|
+
That page shows the site with a countdown and signs the user in to keep it.
|
|
160
|
+
Treat the link as a secret — whoever opens it while signed in to Wix claims the
|
|
161
|
+
site into *their* account — so give it only to the user who asked.
|
|
22
162
|
|
|
23
|
-
|
|
24
|
-
2. Wix hosts them immediately on a live URL they can open and share.
|
|
163
|
+
### 4. Put the site in the user's account
|
|
25
164
|
|
|
26
|
-
|
|
165
|
+
```bash
|
|
166
|
+
curl -sS -X POST \
|
|
167
|
+
"https://www.wixapis.com/headless-business-setup/v1/headless-business/anonymous/$ANONYMOUS_ID/$META_SITE_ID/claim" \
|
|
168
|
+
-H "Authorization: Bearer $ACCESS_TOKEN"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Returns `{}` on success; needs a user or account-level identity (else
|
|
172
|
+
`UNAUTHENTICATED`). It consumes the anonymous record, so it's the last call that
|
|
173
|
+
works against these endpoints — do it after the release, never before.
|
|
174
|
+
|
|
175
|
+
**The site's URL changes here.** The step-3 host stops resolving — the free host
|
|
176
|
+
is scoped to the owning account and re-minted under the user's. Read the new one
|
|
177
|
+
in step 5; never repeat the step-3 URL after a claim.
|
|
178
|
+
|
|
179
|
+
### 5. Read the site's final URL
|
|
180
|
+
|
|
181
|
+
[Query Sites](https://dev.wix.com/docs/api-reference/account-level/sites/sites/query-sites)
|
|
182
|
+
filtered to the **`HEADLESS` namespace** — headless sites are omitted from the
|
|
183
|
+
default query, so the filter is required, and an `id` filter is rejected, so match
|
|
184
|
+
the id yourself.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
curl -sS -X POST "https://www.wixapis.com/site-list/v2/sites/query" \
|
|
188
|
+
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
|
189
|
+
-H 'Content-Type: application/json' \
|
|
190
|
+
-d '{"query":{"filter":{"namespace":"HEADLESS"},"cursorPaging":{"limit":100}}}'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Find the entry whose `id` equals your `metaSiteId`, read `viewUrl` (page with
|
|
194
|
+
`metadata.cursors.next` if needed):
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{ "id": "f0ad8672-09e9-4111-b6fe-070c70bd2df5",
|
|
198
|
+
"displayName": "Snake", "published": true, "namespace": "HEADLESS",
|
|
199
|
+
"viewUrl": "https://instant-hguwrvtcrniw-ayalg5-1406.wix-site-host.com/" }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Give the user two links: `viewUrl` for the live site, and its dashboard at
|
|
203
|
+
`https://manage.wix.com/dashboard/{metaSiteId}` to manage it.
|
|
204
|
+
|
|
205
|
+
### What the upload accepts, and how it fails
|
|
206
|
+
|
|
207
|
+
The requirements below apply to both paths — an upload that breaks them is why a
|
|
208
|
+
release never happens:
|
|
209
|
+
|
|
210
|
+
- **A top-level HTML file is required.** It needn't be named `index.html` (a lone
|
|
211
|
+
top-level HTML of any name becomes the homepage), but once there's more than
|
|
212
|
+
one, an `index.html` must be among them.
|
|
213
|
+
- **3 MB per file, 20 MB per site.**
|
|
214
|
+
- **Static files only** — HTML, CSS, JS, images, fonts. Framework source that
|
|
215
|
+
needs a build step (a `package.json`, React/Vue sources) must be built first;
|
|
216
|
+
upload the build output.
|
|
217
|
+
|
|
218
|
+
On Path B, failures come back as HTTP 400 with a code in
|
|
219
|
+
`details.applicationError.code`: `MISSING_INDEX_HTML` (add a top-level HTML),
|
|
220
|
+
`FILE_TOO_LARGE` (a file over 3 MB), `TOTAL_TOO_LARGE` (bundle over 20 MB). A
|
|
221
|
+
`404` after step 1 means the one-hour window passed or the site was already
|
|
222
|
+
claimed — start again from step 1.
|
|
223
|
+
|
|
224
|
+
**If Path B fails for any reason you can't quickly fix — a rejected upload, an
|
|
225
|
+
error, a call you can't make — fall back to [Path A](#path-a--hand-the-user-the-drop-page):
|
|
226
|
+
give the user the drop-page link so they can finish in the browser.** Never leave
|
|
227
|
+
them with a failed publish and no way forward; the drop page reaches the same
|
|
228
|
+
result without the step that broke.
|
|
229
|
+
|
|
230
|
+
## Keep building: add a backend when you need one
|
|
231
|
+
|
|
232
|
+
The drop flow publishes **static** files. When the site needs a real backend —
|
|
233
|
+
stores, payments, bookings, a CMS, members, forms — it becomes a **Wix Headless
|
|
234
|
+
project**. This is a solution the user chooses when the need appears, not a
|
|
235
|
+
required step: a static site is a finished result on its own.
|
|
236
|
+
|
|
237
|
+
Offer it once the site is claimed/owned and you're in a coding environment with a
|
|
238
|
+
shell. It keeps the **same site, appId and URL** (the downloaded project already
|
|
239
|
+
carries a `wix.config.json` binding it), and is released with the Wix CLI from
|
|
240
|
+
then on:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
curl -sSL -o project.zip \
|
|
244
|
+
"https://www.wix.com/_api/wixstro-deployments/v1/instant-sites/$META_SITE_ID/download.zip"
|
|
245
|
+
unzip project.zip -d project # index.html + assets + wix.config.json
|
|
246
|
+
```
|
|
27
247
|
|
|
28
|
-
|
|
29
|
-
|
|
248
|
+
Then follow `https://wix.com/headless/skill.md`: it turns the static files into a
|
|
249
|
+
headless project (`@wix/sdk` + the business solutions the site needs), bound to
|
|
250
|
+
the same site.
|
|
30
251
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- **Static files only** — HTML, CSS, JavaScript, and assets. If the user has
|
|
38
|
-
framework source code that needs a build step (a `package.json`, React/Vue
|
|
39
|
-
source, etc.), they must run their build first and upload the build output
|
|
40
|
-
folder's contents, not the source.
|
|
252
|
+
This is the only way to keep changing a **claimed** site in code, and it's why it
|
|
253
|
+
needs `wix login`: re-releasing to a claimed site is authenticated *and*
|
|
254
|
+
file-based, and an authenticated file operation needs the token in your **shell**
|
|
255
|
+
(via `wix login`), not injected into a tool — the API-call/execute-API tools carry
|
|
256
|
+
the auth but can't do file operations. There's no lighter "re-upload static to my
|
|
257
|
+
claimed site" shortcut.
|
|
41
258
|
|
|
42
259
|
## Route the request correctly
|
|
43
260
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
261
|
+
- **You can't read the files, can't make HTTP calls, or have no shell
|
|
262
|
+
(API-call/execute-API tools only, which can't upload a file)** — Path A, the
|
|
263
|
+
drop page. It's also the default whenever Path B isn't clearly available, and
|
|
264
|
+
the fallback whenever Path B fails partway — hand over the link, don't leave the
|
|
265
|
+
user stuck.
|
|
266
|
+
- **Files you generated here or can read, and you can upload from a shell** —
|
|
267
|
+
Path B. If you hold the user's identity, claim it into their account and return
|
|
268
|
+
the live URL + dashboard.
|
|
269
|
+
- **A change to a site you published this way here** —
|
|
270
|
+
[iterate in place](#iterate-while-anonymous-dont-create-a-new-site) on the same ids.
|
|
271
|
+
- **A published anonymous site the user wants to keep** — steps 4–5 with their
|
|
272
|
+
identity, else the step-3 save link.
|
|
273
|
+
- **A claimed site that now needs a backend** (stores, bookings, CMS, members,
|
|
274
|
+
forms) — [add a backend](#keep-building-add-a-backend-when-you-need-one).
|
|
275
|
+
- **Migrating a live site/store from another platform by URL, or CSV/TSV
|
|
276
|
+
exports** — [Site Import](site-import.md).
|
|
277
|
+
- **Adding HTML, an embed, or code to an existing Wix site** — not this recipe
|
|
278
|
+
(that's custom code in the editor).
|
|
279
|
+
- **Images, videos, or documents for a site** —
|
|
280
|
+
[Upload Media to Wix](../media/upload-media-to-wix.md).
|
|
50
281
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
is the flow.
|
|
282
|
+
Don't create the site with other tools (meta-site templates, headless-business
|
|
283
|
+
provisioning) — those make an empty site, not a published copy of the user's files.
|
|
@@ -103,18 +103,22 @@ export default function ProductCard({ product }) {
|
|
|
103
103
|
const {
|
|
104
104
|
isSoldOut, isPreorder, isPartiallyOutOfStock, // booleans
|
|
105
105
|
leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
ribbons, // every merchant ribbon label, primary first — render ALL of them, one shared style
|
|
107
|
+
promoBadge, // { type: 'ribbon', label } | null — the primary ribbon, for a single-badge slot
|
|
108
|
+
priceDisplay, // the price the buyer pays (discounted when an automatic discount applies), or a min–max range
|
|
109
|
+
compareAtDisplay, // struck "was" price | null — never beside a range (the PDP shows the real comparison)
|
|
109
110
|
colors, // hex colour strings
|
|
110
111
|
optionLabel, // e.g. "3 sizes · 2 materials", or ""
|
|
111
112
|
isQuickAddable, // no product options and not sold out; does NOT check modifiers
|
|
113
|
+
directAddVariantId, // the variant a quick-add sends; null when not quick-addable
|
|
112
114
|
image, hoverImage,// normalised primary / second gallery URL | null
|
|
113
115
|
} = useProductCard(product);
|
|
114
116
|
// Render your card.
|
|
115
117
|
}
|
|
116
118
|
```
|
|
117
|
-
|
|
119
|
+
A ribbon is a label, never proof of a price: don't render a "Sale" badge from `compareAtDisplay`,
|
|
120
|
+
and don't compute a percent-off — the hook deliberately doesn't. For quick-add, call
|
|
121
|
+
`addToCart(product.id, directAddVariantId)` only when `isQuickAddable` and no mandatory
|
|
118
122
|
modifier needs input (`product.modifiers` entries have a `mandatory` boolean). Otherwise link to `/product/${product.slug}` for selection, including
|
|
119
123
|
pre-orders. Listing results have no full variants; the PDP hook loads and resolves them.
|
|
120
124
|
Use cart-context `loading` to disable repeated adds. Failures open the shipped drawer with
|
|
@@ -198,15 +202,19 @@ export default function ProductDetail() {
|
|
|
198
202
|
// d contains:
|
|
199
203
|
// product: Wix product | null; .name, .slug, .plainDescription (HTML)
|
|
200
204
|
// notFound: boolean; error: string | null (load error); retry: () => void
|
|
201
|
-
// price
|
|
205
|
+
// price: formatted — the product's min–max range until every option is picked, then the selected
|
|
206
|
+
// variant's price (an automatic discount beats the regular price); compareAtPrice: the struck
|
|
207
|
+
// "was" price, "" when none. discountNames: automatic-discount names, product level.
|
|
202
208
|
// options, modifiers: arrays consumed by useVariantOptions above
|
|
203
|
-
// selectedOptions: { [optionId]: choiceId }; selectOption(optionId, choiceId)
|
|
209
|
+
// selectedOptions: { [optionId]: choiceId } — starts EMPTY (no pre-picked choice); selectOption(optionId, choiceId)
|
|
204
210
|
// modifierValues: { [key]: value }; setModifier(key, value)
|
|
205
211
|
// variant: resolved variant | null; null if required choices are missing or no match exists
|
|
206
212
|
// focusMediaUrl: selected choice image, then variant image, or null
|
|
207
|
-
// quantity, setQuantity(n): number (may be "" mid-edit)
|
|
208
|
-
// inStock, canAdd, adding: booleans
|
|
209
|
-
//
|
|
213
|
+
// quantity, setQuantity(n): number (may be "" mid-edit); resets to 1 when an option changes
|
|
214
|
+
// inStock, isPreorder, canAdd, adding: booleans — a pre-order variant IS addable
|
|
215
|
+
// blockedReason: string | null — why the buy button is disabled ("Choose Size", "Out of stock");
|
|
216
|
+
// render it next to the action as neutral guidance, not an error, until the buyer tries to buy
|
|
217
|
+
// submit: async () => adds product + resolved variant + quantity + modifiers (+ preorder)
|
|
210
218
|
// resolves undefined on success, null on failure (not a cart object or a boolean)
|
|
211
219
|
// Replace these placeholders with your error, not-found, and loading UI.
|
|
212
220
|
if (d.error) return null; // show d.error and offer d.retry()
|
|
@@ -220,24 +228,28 @@ export default function ProductDetail() {
|
|
|
220
228
|
```
|
|
221
229
|
- Render `product.plainDescription` as HTML; strike `compareAtPrice` only when present and different from `price`.
|
|
222
230
|
- Make the gallery follow `focusMediaUrl` when the selected option changes.
|
|
223
|
-
- Render the option/modifier controls below;
|
|
224
|
-
- Keep quantity at least 1. Disable adding when `!canAdd || adding`; call `submit()` only with a loaded product and `canAdd`. `submit()` coerces quantity to at least 1 but does not itself enforce `canAdd`.
|
|
225
|
-
- `canAdd` checks variant resolution,
|
|
231
|
+
- Render the option/modifier controls below; mark choices whose `inStock` is false; show `d.blockedReason` beside the button while it's disabled.
|
|
232
|
+
- Label the action by state: `isPreorder` → "Pre-order"; otherwise "Add to cart". Keep quantity at least 1. Disable adding when `!canAdd || adding`; call `submit()` only with a loaded product and `canAdd`. `submit()` coerces quantity to at least 1 but does not itself enforce `canAdd`.
|
|
233
|
+
- `canAdd` checks variant resolution, availability (in stock or pre-orderable), and mandatory modifier values. `inStock` defaults to true without a resolved variant; use `canAdd` for the full gate.
|
|
234
|
+
- Render the PDP gallery from `images` — every image reachable (thumbnails, arrows, or a swipeable rail), not just the first; a single-image product gets no empty strip.
|
|
226
235
|
- `submit()` resets `adding` after completion and preserves the cart result: `undefined` on success, `null` on failure. Add failures live in `useCart().error`, not the PDP load `error`; the shipped drawer opens to display them.
|
|
227
236
|
|
|
228
|
-
For an optional **
|
|
237
|
+
For an optional **add and check out** shortcut, check the add result before checkout:
|
|
229
238
|
```jsx
|
|
230
239
|
// Inside ProductDetail, with the other hooks above any conditional returns:
|
|
231
240
|
const { checkout, loading: cartLoading } = useCart(); // named import from @/context/CartContext
|
|
232
|
-
async function
|
|
241
|
+
async function addAndCheckout() {
|
|
233
242
|
if (!d.product || !d.canAdd || d.adding || cartLoading) return;
|
|
234
243
|
const result = await d.submit();
|
|
235
244
|
if (result === null) return; // add failed; preserve its error and do not check out the old cart
|
|
236
245
|
await checkout();
|
|
237
246
|
}
|
|
238
|
-
// <button disabled={!d.canAdd || d.adding || cartLoading} onClick={
|
|
247
|
+
// <button disabled={!d.canAdd || d.adding || cartLoading} onClick={addAndCheckout}>Add and check out</button>
|
|
239
248
|
```
|
|
240
|
-
|
|
249
|
+
This adds to the shopper's **current cart** and checks that cart out — so label it that way. A true
|
|
250
|
+
**Buy Now** (skip the cart, buy just this item) needs a standalone cart handed to the redirect session;
|
|
251
|
+
it is not shipped, and routing "Buy now" through the current cart is wrong (it would carry whatever the
|
|
252
|
+
cart already holds). For direct `addToCart(...)` calls, use the same `result === null` check before checkout.
|
|
241
253
|
A resolved promise alone does not mean success; a truthiness check also rejects successful `undefined`.
|
|
242
254
|
|
|
243
255
|
### Variant and modifier controls
|
|
@@ -277,14 +289,17 @@ within `CartProvider` (the PDP hook calls it for you).
|
|
|
277
289
|
|
|
278
290
|
```js
|
|
279
291
|
const { addToCart, isOpen, setIsOpen, loading, error, clearError } = useCart();
|
|
280
|
-
// addToCart(productId, variantId?, qty = 1, { modifierChoices?, customTextFields? }?)
|
|
292
|
+
// addToCart(productId, variantId?, qty = 1, { modifierChoices?, customTextFields?, subscriptionOptionId?, preorder? }?)
|
|
281
293
|
// isOpen: boolean; setIsOpen(true): open the drawer; setIsOpen(false): close it
|
|
282
294
|
// loading: mutation-in-progress boolean; disable repeated adds while true
|
|
283
295
|
// error: string | null; clearError(): clears it
|
|
284
296
|
```
|
|
285
|
-
`addToCart` uses `product.id` and the resolved `variant.id` when needed. Extras
|
|
297
|
+
`addToCart` uses `product.id` and the resolved `variant.id` when needed. Extras: string maps
|
|
286
298
|
`modifierChoices: { [modifier.key]: choiceKey }` and
|
|
287
|
-
`customTextFields: { [modifier.freeTextSettings.key]: userInput }
|
|
299
|
+
`customTextFields: { [modifier.freeTextSettings.key]: userInput }` (include mandatory values);
|
|
300
|
+
`preorder: true` for a pre-order variant (the PDP hook sets it); `subscriptionOptionId` for a chosen
|
|
301
|
+
recurring plan (omit for one-time). The shipped PDP hook doesn't render subscriptions — add that
|
|
302
|
+
only when the catalog has `subscriptionPricesInfo`, requiring an explicit plan choice.
|
|
288
303
|
|
|
289
304
|
The context's add/remove/update/checkout methods return promises resolving to `undefined` on
|
|
290
305
|
success or `null` on failure, storing the failure in `error`. They clear the previous error and
|
|
@@ -298,8 +313,10 @@ Only use these additional contracts if you choose to render your own cart surfac
|
|
|
298
313
|
from the same `useCart()` context; mutation results and error handling are described above.
|
|
299
314
|
|
|
300
315
|
```js
|
|
301
|
-
const { cart, itemCount, removeItem, updateQuantity, checkout, refreshCart } = useCart();
|
|
316
|
+
const { cart, summary, itemCount, removeItem, updateQuantity, checkout, refreshCart } = useCart();
|
|
302
317
|
// cart: server cart | null; itemCount: sum of confirmed line quantities
|
|
318
|
+
// summary: the estimate's { priceSummary: { subtotal, discount, total } } | null — refreshed with the cart;
|
|
319
|
+
// subtotal is after discounts, discount is the CART-level discount only; both ConvertedMoney
|
|
303
320
|
// removeItem(lineItemId); updateQuantity(lineItemId, qty)
|
|
304
321
|
// checkout(); refreshCart()
|
|
305
322
|
```
|
|
@@ -307,7 +324,8 @@ For custom cart presentation, `cart?.lineItems ?? []` is the list. Each line has
|
|
|
307
324
|
`name.original`, and optional `attributes.image.url`. Option/modifier labels are in
|
|
308
325
|
`attributes.descriptionLines`: `[{ name: { original }, plainText?: { original },
|
|
309
326
|
colorInfo?: { original, code } }]`; use `plainText.original` or `colorInfo.original` for the value.
|
|
310
|
-
Update/remove use the line's `id`, not a catalog product id.
|
|
327
|
+
Update/remove use the line's `id`, not a catalog product id. Show each line's `attributes.descriptionLines`
|
|
328
|
+
(the chosen options and text) — a shopper verifies the configuration in the cart, not the checkout.
|
|
311
329
|
|
|
312
330
|
Normalize custom cart images with `storeImage(line.attributes?.image?.url)` (see **Images**).
|
|
313
331
|
|
|
@@ -320,10 +338,10 @@ Cart money is `{ amount, convertedAmount }`, with no formatted string. `amount`
|
|
|
320
338
|
currency; `convertedAmount` is in display currency. Format
|
|
321
339
|
`money.convertedAmount ?? money.amount` with `Intl.NumberFormat` and
|
|
322
340
|
`cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode` (the shipped drawer falls
|
|
323
|
-
back to `USD`). Use
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
341
|
+
back to `USD`). Use `summary.priceSummary.subtotal` (after discounts) with `cart.subtotal` as the
|
|
342
|
+
fallback while `summary` is null, a Discount row only when `priceSummary.discount` is above zero, and
|
|
343
|
+
line `pricing.totalPrice`; never sum lines yourself. Shipping and tax resolve at checkout — say
|
|
344
|
+
"calculated at checkout", never a hardcoded charge, threshold, or a made-up zero.
|
|
327
345
|
|
|
328
346
|
Line `quantityInfo.confirmedQuantity` is the current quantity; `availableQuantity` caps increases
|
|
329
347
|
when finite. `status` can be `IN_STOCK`, `PARTIALLY_IN_STOCK`, `OUT_OF_STOCK`, or
|
|
@@ -416,6 +434,28 @@ function Layout() {
|
|
|
416
434
|
}
|
|
417
435
|
```
|
|
418
436
|
|
|
437
|
+
## What a complete storefront shows
|
|
438
|
+
|
|
439
|
+
The wiring above is necessary, not sufficient. These are defaults for when the brief doesn't say
|
|
440
|
+
otherwise — whatever the user asked for in their prompt wins over any line here. Look at the
|
|
441
|
+
catalog before designing — categories, assortment size, media, options, sales — and design for this
|
|
442
|
+
store, not for a stereotype of its category. Then:
|
|
443
|
+
|
|
444
|
+
- **Home:** what the store sells and one shopping action in the first screen; real products from
|
|
445
|
+
the catalog under truthful headings; not a repeat of the shop page.
|
|
446
|
+
- **Shop:** a real product card — image, name, price, link — in the first screen; sort and the
|
|
447
|
+
filters the catalog supports; loading, empty, no-results, and error states that look different.
|
|
448
|
+
- **Product page:** image, name, price, the first choice, and the buy button with `blockedReason`
|
|
449
|
+
in the first screen; every ribbon; every image reachable in the gallery.
|
|
450
|
+
- **Cart:** the shipped drawer — it opens after every add, and checkout is a button in it.
|
|
451
|
+
- **Overlays you build** (quick-add, mobile nav, filters): mount at the document root, lock
|
|
452
|
+
background scroll, close on Escape, return focus on close — as the shipped `CartDrawer` does.
|
|
453
|
+
- **Copy:** nothing the merchant didn't supply — no invented reviews, scarcity, or delivery
|
|
454
|
+
promises; no Wix IDs or technical words in visible text.
|
|
455
|
+
|
|
456
|
+
Pre-order ships (`isPreorder`). Subscriptions, product groups, promotions, and notify-me are built
|
|
457
|
+
only when the catalog has them — never fabricated; consult the documentation skill for their endpoints.
|
|
458
|
+
|
|
419
459
|
## Missing capabilities
|
|
420
460
|
For anything these interfaces do not cover, consult the official Wix API documentation using
|
|
421
461
|
the documentation skill available in your environment.
|
|
@@ -425,4 +465,6 @@ shipped file; catalog and cart helpers link their API references inline.
|
|
|
425
465
|
## Hard rules
|
|
426
466
|
- Header/footer live in a `Layout` around `<Outlet/>` (see **Routes and provider**) — keep shared chrome out of individual pages.
|
|
427
467
|
- Checkout goes through the shipped cart (redirect-session) — never a hand-built `/checkout` URL.
|
|
428
|
-
- Render live Wix data or your empty state — never mock products.
|
|
468
|
+
- Render live Wix data or your empty state — never mock products, reviews, ratings, or counts.
|
|
469
|
+
- Prices and ribbons come from the hooks as-is: no computed percent-off, no "Sale" badge inferred from a compare-at price, no struck price beside a range.
|
|
470
|
+
- Cart totals come from `summary`/`cart` — never summed or hardcoded in the client; shipping and tax say "calculated at checkout".
|
|
@@ -5,15 +5,14 @@
|
|
|
5
5
|
// • prices come from the SERVER cart. In Cart V2 each line total is `pricing.totalPrice`, a
|
|
6
6
|
// ConvertedMoney { amount, convertedAmount } with NO formatted string — format it yourself with the
|
|
7
7
|
// cart's currency (see formatCartMoney). Never compute a total in the client: tax, shipping and
|
|
8
|
-
// promotions are resolved server-side
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// cart; absent that call, show the raw `subtotal`.
|
|
8
|
+
// promotions are resolved server-side. The V2 cart has no `subtotalAfterDiscounts`/`discount` —
|
|
9
|
+
// the after-discount subtotal and the cart-level discount come from the estimate the context keeps
|
|
10
|
+
// in `summary.priceSummary`; the raw cart `subtotal` is the fallback while it's unknown.
|
|
12
11
|
// • `quantityInfo.availableQuantity` caps the stepper, so exceeding stock is refused here rather than
|
|
13
12
|
// at checkout.
|
|
14
13
|
// • `status` is flagged per line here because checkout() refuses the whole cart when any item isn't
|
|
15
14
|
// IN_STOCK — showing it on the row is what makes that refusal understandable.
|
|
16
|
-
import { useEffect } from "react";
|
|
15
|
+
import { useEffect, useRef } from "react";
|
|
17
16
|
import { useCart } from "@/context/CartContext";
|
|
18
17
|
import { storeImage } from "@/lib/storeImage";
|
|
19
18
|
|
|
@@ -26,28 +25,41 @@ function formatCartMoney(money, cart) {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
export default function CartDrawer() {
|
|
29
|
-
const { cart, isOpen, setIsOpen, removeItem, updateQuantity, checkout, loading, error, clearError } = useCart();
|
|
28
|
+
const { cart, summary, isOpen, setIsOpen, removeItem, updateQuantity, checkout, loading, error, clearError } = useCart();
|
|
30
29
|
const lineItems = cart?.lineItems ?? [];
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
30
|
+
// Totals come from the estimate the context keeps next to the cart (after-discount subtotal, the
|
|
31
|
+
// cart-level discount). While it's unknown, or if it failed, fall back to the cart's raw subtotal.
|
|
32
|
+
// Delivery and tax are NOT in here — they resolve at checkout, and the footer says so.
|
|
33
|
+
const subtotal = formatCartMoney(summary?.priceSummary?.subtotal ?? cart?.subtotal, cart);
|
|
34
|
+
const discountAmount = Number(summary?.priceSummary?.discount?.convertedAmount ?? summary?.priceSummary?.discount?.amount ?? 0);
|
|
35
|
+
const discount = discountAmount > 0 ? formatCartMoney(summary.priceSummary.discount, cart) : "";
|
|
34
36
|
const unavailable = lineItems.filter((li) => li.status && li.status !== "IN_STOCK");
|
|
35
37
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
+
// The overlay contract, verified in the browser rather than assumed from CSS: Escape closes (the
|
|
39
|
+
// backdrop click is pointer-only), the page behind stops scrolling, focus moves into the panel on
|
|
40
|
+
// open and returns to whatever opened it on close.
|
|
41
|
+
const panelRef = useRef(null);
|
|
38
42
|
useEffect(() => {
|
|
39
43
|
if (!isOpen) return;
|
|
44
|
+
const opener = document.activeElement;
|
|
45
|
+
const previousOverflow = document.body.style.overflow;
|
|
46
|
+
document.body.style.overflow = "hidden";
|
|
47
|
+
panelRef.current?.focus();
|
|
40
48
|
const onKey = (e) => e.key === "Escape" && setIsOpen(false);
|
|
41
49
|
document.addEventListener("keydown", onKey);
|
|
42
|
-
return () =>
|
|
50
|
+
return () => {
|
|
51
|
+
document.removeEventListener("keydown", onKey);
|
|
52
|
+
document.body.style.overflow = previousOverflow;
|
|
53
|
+
if (opener instanceof HTMLElement) opener.focus();
|
|
54
|
+
};
|
|
43
55
|
}, [isOpen, setIsOpen]);
|
|
44
56
|
|
|
45
57
|
if (!isOpen) return null;
|
|
46
58
|
|
|
47
59
|
return (
|
|
48
60
|
<div onClick={() => setIsOpen(false)} className="fixed inset-0 z-50 bg-black/40 flex justify-end">
|
|
49
|
-
<aside onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Your cart"
|
|
50
|
-
className="w-[min(420px,100%)] h-full flex flex-col bg-background text-foreground border-l border-border font-body">
|
|
61
|
+
<aside ref={panelRef} tabIndex={-1} onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Your cart"
|
|
62
|
+
className="w-[min(420px,100%)] h-full flex flex-col bg-background text-foreground border-l border-border font-body outline-none">
|
|
51
63
|
<header className="flex justify-between items-center p-4 border-b border-border">
|
|
52
64
|
<strong className="font-display">Your cart{lineItems.length ? ` (${lineItems.length})` : ""}</strong>
|
|
53
65
|
<button onClick={() => setIsOpen(false)} aria-label="Close cart"
|
|
@@ -123,6 +135,12 @@ export default function CartDrawer() {
|
|
|
123
135
|
|
|
124
136
|
{lineItems.length > 0 && (
|
|
125
137
|
<footer className="p-4 border-t border-border flex flex-col gap-3">
|
|
138
|
+
{discount && (
|
|
139
|
+
<div className="flex justify-between items-baseline text-sm">
|
|
140
|
+
<span className="text-muted-foreground">Discount</span>
|
|
141
|
+
<span>−{discount}</span>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
126
144
|
{subtotal && (
|
|
127
145
|
<div className="flex justify-between items-baseline">
|
|
128
146
|
<span className="text-muted-foreground">Subtotal</span>
|