create-cartbase 0.1.16 → 0.1.18
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/LICENSE +21 -21
- package/README.md +25 -25
- package/dist/index.js +20 -20
- package/package.json +1 -1
- package/template/app/docs/auth.md +105 -105
- package/template/app/docs/carts.md +376 -376
- package/template/app/docs/categories.md +194 -194
- package/template/app/docs/checkout.md +714 -714
- package/template/app/docs/components.md +55 -11
- package/template/app/docs/consent.md +91 -91
- package/template/app/docs/deploy.md +197 -197
- package/template/app/docs/gift-cards.md +153 -153
- package/template/app/docs/metaobjects.md +126 -126
- package/template/app/docs/orders.md +221 -221
- package/template/app/docs/products.md +51 -2
- package/template/app/docs/regions.md +269 -269
- package/template/app/docs/reviews.md +223 -223
- package/template/app/docs/search.md +227 -227
- package/template/app/docs/store.md +47 -47
- package/template/app/docs/subscriptions.md +148 -148
- package/template/app/docs/variables.md +315 -315
- package/template/app/package.json +1 -1
- package/template/app/postcss.config.cjs +11 -11
- package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
- package/template/app/src/app/checkout/page.tsx +48 -48
- package/template/app/src/app/globals.css +26 -26
- package/template/app/src/app/page.tsx +28 -28
- package/template/app/src/app/products/[handle]/page.tsx +87 -87
- package/template/app/src/app/providers.tsx +68 -64
- package/template/app/src/app/search/page.tsx +23 -23
- package/template/app/src/lib/browser-client.ts +35 -35
- package/template/app/src/lib/config.ts +41 -41
- package/template/app/src/lib/server-client.ts +25 -25
- package/template/app/src/lib/cart-actions.ts +0 -47
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
# Search
|
|
2
|
-
|
|
3
|
-
Configurable storefront search (search-discovery card) plus the PDP's
|
|
4
|
-
related-products rail. Results are the SAME canonical product objects the
|
|
5
|
-
products listing serves (see products.md) — reuse your product-card renderer
|
|
6
|
-
as-is. Money is EUR decimal major units.
|
|
7
|
-
|
|
8
|
-
SDK module: `@cartbase/storefront/api/search`.
|
|
9
|
-
|
|
10
|
-
**Matching** — title/subtitle/description full-text (Cyrillic-correct
|
|
11
|
-
'simple' config, prefix match) ∪ typo tolerance via trigram similarity on
|
|
12
|
-
the title ("linnen" finds "Linen Shirt") ∪ SKU prefix ∪ exact tag value.
|
|
13
|
-
Draft products, other tenants, and products outside the publishable key's
|
|
14
|
-
channels NEVER appear.
|
|
15
|
-
|
|
16
|
-
**Synonyms (admin-authored)** — expansion is BIDIRECTIONAL and SINGLE-LEVEL
|
|
17
|
-
(bounded — expansions never re-expand; capped at 24 terms): a query word
|
|
18
|
-
matching a row's term adds its synonyms; a query word matching one of a
|
|
19
|
-
row's synonyms adds the term + its sibling synonyms. So with the admin row
|
|
20
|
-
`term: "зехтин", synonyms: ["olive oil"]`, EITHER direction finds the
|
|
21
|
-
product. Terms are normalized lowercase.
|
|
22
|
-
|
|
23
|
-
**Pins & boosts (admin-authored)** — ordering is: (1) pins for this EXACT
|
|
24
|
-
normalized query, in position order — pins are INJECTED even without a text
|
|
25
|
-
match, but only if they pass filters + channel scope + published (the leak
|
|
26
|
-
rule applies to pins too); then (2) globally-boosted products WITHIN the
|
|
27
|
-
match set, in position order; then (3) relevance rank with a deterministic
|
|
28
|
-
tie-break. Global boosts reorder, never inject.
|
|
29
|
-
|
|
30
|
-
**Cache rule** — responses carry `calculated_price` when a pricing context
|
|
31
|
-
is given; they vary by customer group — never cache shared when a JWT was
|
|
32
|
-
present.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## GET /api/store/products/search
|
|
37
|
-
|
|
38
|
-
- **Purpose** — the search results page + autocomplete backend.
|
|
39
|
-
- **Auth** — anon: `x-client-id` required; `x-publishable-api-key` optional
|
|
40
|
-
(channel scope, products-listing semantics); Bearer JWT optional (group
|
|
41
|
-
pricing).
|
|
42
|
-
- **Request**
|
|
43
|
-
|
|
44
|
-
```jsonc
|
|
45
|
-
// query — q is REQUIRED (min 1 char), everything else optional
|
|
46
|
-
{
|
|
47
|
-
"q": "linen",
|
|
48
|
-
"collection_id": "pcol_a,pcol_b", // CSV
|
|
49
|
-
"category_id": "pcat_a", // CSV
|
|
50
|
-
"tag_id": "ptag_a", // CSV
|
|
51
|
-
"type_id": "ptyp_a", // CSV
|
|
52
|
-
"price_min": 10, // major units, on the product's cheapest base price
|
|
53
|
-
"price_max": 50,
|
|
54
|
-
"availability": "in_stock", // in_stock | out_of_stock
|
|
55
|
-
// Option filters — dynamic keys, repeatable; OR within an option, AND
|
|
56
|
-
// across options. URL-encode titles ("Length (cm)" → option.Length%20(cm)):
|
|
57
|
-
"option.Size": "S,M",
|
|
58
|
-
"currency_code": "eur", // pricing context (default price facet currency: eur)
|
|
59
|
-
"region_id": "reg_…",
|
|
60
|
-
"limit": 20, // 1–100, default 20
|
|
61
|
-
"offset": 0
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
- **Response**
|
|
66
|
-
|
|
67
|
-
```jsonc
|
|
68
|
-
{
|
|
69
|
-
"results": [ /* canonical product objects — see products.md */ ],
|
|
70
|
-
"facets": [
|
|
71
|
-
{
|
|
72
|
-
"key": "price", // "price" | "availability" | "type" |
|
|
73
|
-
// "tags" | "collection" | "options.<Title>"
|
|
74
|
-
"label": "Price",
|
|
75
|
-
"type": "range", // "range" for price, "value" otherwise
|
|
76
|
-
"buckets": [
|
|
77
|
-
{ "value": "20-40", "label": "€20 – €40", "count": 3,
|
|
78
|
-
"min": 20, "max": 40 } // min/max on range buckets only; max null = open top
|
|
79
|
-
]
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
"key": "availability", "label": "Availability", "type": "value",
|
|
83
|
-
"buckets": [ { "value": "in_stock", "label": "In stock", "count": 5 } ]
|
|
84
|
-
}
|
|
85
|
-
],
|
|
86
|
-
"total": 5,
|
|
87
|
-
"offset": 0,
|
|
88
|
-
"limit": 20
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Facets** follow the tenant's Settings → Search & discovery → Filters
|
|
93
|
-
config (order + enabled; defaults: price, availability, type, tags,
|
|
94
|
-
collection). **The faceting count rule:** each facet's bucket counts are
|
|
95
|
-
computed on the result set filtered by every OTHER active filter — the
|
|
96
|
-
facet's own dimension is excluded, so selecting "Size: M" never zeroes the
|
|
97
|
-
other sizes. Empty buckets are omitted; a facet with no buckets is
|
|
98
|
-
omitted (the dev tenant has no tags/types, so those facets simply don't
|
|
99
|
-
appear). Price buckets: `auto` (deterministic nice-number split of
|
|
100
|
-
observed prices, ≤10 buckets) or `fixed` admin ranges; apply one by
|
|
101
|
-
passing its `min`/`max` back as `price_min`/`price_max`. Value-facet
|
|
102
|
-
buckets carry ids as `value` (type/tags/collection) or the option value
|
|
103
|
-
string (`options.<Title>`).
|
|
104
|
-
|
|
105
|
-
- **Working curl** — seeded catalog: Linen Shirt (45), Wool Beanie (23),
|
|
106
|
-
Leather Belt (60), all in the `essentials` collection:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
SEARCH=$(curl -sf "$BASE/api/store/products/search?q=linen¤cy_code=eur" \
|
|
110
|
-
-H "x-client-id: $CLIENT_ID")
|
|
111
|
-
echo "$SEARCH" | grep -q '"results"'
|
|
112
|
-
echo "$SEARCH" | grep -q '"facets"'
|
|
113
|
-
echo "$SEARCH" | grep -q '"total"'
|
|
114
|
-
echo "$SEARCH" | grep -q '"handle":"linen-shirt"'
|
|
115
|
-
echo "$SEARCH" | grep -q '"calculated_price"'
|
|
116
|
-
# Default facet config puts price + availability on a priced, stocked catalog:
|
|
117
|
-
echo "$SEARCH" | grep -q '"key":"price"'
|
|
118
|
-
echo "$SEARCH" | grep -q '"key":"availability"'
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
# Typo tolerance (title trigram): "linnen" still finds the Linen Shirt.
|
|
123
|
-
curl -sf "$BASE/api/store/products/search?q=linnen" -H "x-client-id: $CLIENT_ID" \
|
|
124
|
-
| grep -q '"handle":"linen-shirt"'
|
|
125
|
-
# SKU prefix match:
|
|
126
|
-
curl -sf "$BASE/api/store/products/search?q=LIN-SHIRT" -H "x-client-id: $CLIENT_ID" \
|
|
127
|
-
| grep -q '"handle":"linen-shirt"'
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
# Option filter: only the Linen Shirt has Size S; the beanie (Color) and the
|
|
132
|
-
# belt (Length (cm)) don't match option.Size at all.
|
|
133
|
-
OPT=$(curl -sf "$BASE/api/store/products/search?q=linen&option.Size=S" \
|
|
134
|
-
-H "x-client-id: $CLIENT_ID")
|
|
135
|
-
echo "$OPT" | grep -q '"handle":"linen-shirt"'
|
|
136
|
-
# A value no variant carries → empty result set (envelope intact):
|
|
137
|
-
curl -sf "$BASE/api/store/products/search?q=linen&option.Size=XXL" \
|
|
138
|
-
-H "x-client-id: $CLIENT_ID" | grep -q '"results":\[\]'
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
# Channel scope: the dev PUBLISHABLE_KEY is bound to the B2B channel
|
|
143
|
-
# (shirt + beanie). "leather" matches anon but returns nothing under the key.
|
|
144
|
-
curl -sf "$BASE/api/store/products/search?q=leather" -H "x-client-id: $CLIENT_ID" \
|
|
145
|
-
| grep -q '"handle":"leather-belt"'
|
|
146
|
-
curl -sf "$BASE/api/store/products/search?q=leather" \
|
|
147
|
-
-H "x-client-id: $CLIENT_ID" -H "x-publishable-api-key: $PUBLISHABLE_KEY" \
|
|
148
|
-
| grep -q '"results":\[\]'
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
- **Errors** — 400 `validation_failed` (missing/empty `q`, bad
|
|
152
|
-
`availability`, limit > 100), 400 `missing_client_id`,
|
|
153
|
-
400 `invalid_publishable_key`, 400 `invalid_region`.
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
157
|
-
"$BASE/api/store/products/search" -H "x-client-id: $CLIENT_ID")
|
|
158
|
-
test "$STATUS" = 400
|
|
159
|
-
curl -s "$BASE/api/store/products/search" -H "x-client-id: $CLIENT_ID" \
|
|
160
|
-
| grep -q '"code":"validation_failed"'
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
- **SDK** — `searchProducts(client, query)` — pass option filters as
|
|
164
|
-
`options: { Size: ["S","M"] }`; the SDK folds them into `option.<Title>`
|
|
165
|
-
params.
|
|
166
|
-
- **Components** — search page, facet sidebar, autocomplete.
|
|
167
|
-
- **Settings** — Search & discovery: synonyms, pins (per-query) + global
|
|
168
|
-
boosts, facet order/enabled, price bucket strategy (auto/fixed).
|
|
169
|
-
|
|
170
|
-
---
|
|
171
|
-
|
|
172
|
-
## GET /api/store/products/:idOrHandle/related
|
|
173
|
-
|
|
174
|
-
- **Purpose** — the PDP's product-to-product rails, BOTH of them,
|
|
175
|
-
discriminated by `kind`:
|
|
176
|
-
- `kind=related` (default) — the rail below the product. Manual admin picks
|
|
177
|
-
first (position order), then a DETERMINISTIC fallback fills to `limit`:
|
|
178
|
-
same primary collection (newest first), then most-shared-tags.
|
|
179
|
-
`auto_filled: true` when any fallback item is present.
|
|
180
|
-
- `kind=complementary` — the "goes with this" slot above the product. The
|
|
181
|
-
merchant's picks in their order and NOTHING else; `auto_filled` is always
|
|
182
|
-
false. There is no fallback by design: similarity can be computed,
|
|
183
|
-
complementarity is a merchant judgement, and a guessed upsell beside the
|
|
184
|
-
buy button is worse than an empty slot. An empty response means render
|
|
185
|
-
nothing.
|
|
186
|
-
- **Auth** — anon: `x-client-id`; optional publishable key — the anchor
|
|
187
|
-
product must be visible to the key, and scoped-away products never appear
|
|
188
|
-
as related items; optional Bearer JWT (group pricing).
|
|
189
|
-
- **Request** — query `{ kind? (related|complementary, default related),
|
|
190
|
-
limit? (1–24, default 12), currency_code?, region_id? }`. Accepts a product
|
|
191
|
-
id (`prod_…`) or handle.
|
|
192
|
-
- **Response**
|
|
193
|
-
|
|
194
|
-
```jsonc
|
|
195
|
-
{
|
|
196
|
-
"products": [ /* canonical product objects, self-excluded */ ],
|
|
197
|
-
"count": 2,
|
|
198
|
-
"auto_filled": true // at least one item came from the fallback chain
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
- **Working curl** — the seeded products share the `essentials` collection,
|
|
203
|
-
so the shirt's rail fills from it:
|
|
204
|
-
|
|
205
|
-
```bash
|
|
206
|
-
RELATED=$(curl -sf "$BASE/api/store/products/linen-shirt/related?currency_code=eur" \
|
|
207
|
-
-H "x-client-id: $CLIENT_ID")
|
|
208
|
-
echo "$RELATED" | grep -q '"products"'
|
|
209
|
-
echo "$RELATED" | grep -q '"auto_filled"'
|
|
210
|
-
echo "$RELATED" | grep -q '"count"'
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
- **Errors** — 404 `not_found` (unknown id/handle, draft, or anchor outside
|
|
214
|
-
the key's channels), 400 `validation_failed`, 400 `invalid_region`.
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
# Channel scope on the ANCHOR: the belt is outside the B2B key's channels.
|
|
218
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
219
|
-
"$BASE/api/store/products/leather-belt/related" \
|
|
220
|
-
-H "x-client-id: $CLIENT_ID" -H "x-publishable-api-key: $PUBLISHABLE_KEY")
|
|
221
|
-
test "$STATUS" = 404
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
- **SDK** — `listRelatedProducts(client, idOrHandle, query?)`.
|
|
225
|
-
- **Components** — PDP related rail.
|
|
226
|
-
- **Settings** — manual picks: Admin → Product → Related (drag-ordered,
|
|
227
|
-
≤24); the fallback chain needs no configuration.
|
|
1
|
+
# Search
|
|
2
|
+
|
|
3
|
+
Configurable storefront search (search-discovery card) plus the PDP's
|
|
4
|
+
related-products rail. Results are the SAME canonical product objects the
|
|
5
|
+
products listing serves (see products.md) — reuse your product-card renderer
|
|
6
|
+
as-is. Money is EUR decimal major units.
|
|
7
|
+
|
|
8
|
+
SDK module: `@cartbase/storefront/api/search`.
|
|
9
|
+
|
|
10
|
+
**Matching** — title/subtitle/description full-text (Cyrillic-correct
|
|
11
|
+
'simple' config, prefix match) ∪ typo tolerance via trigram similarity on
|
|
12
|
+
the title ("linnen" finds "Linen Shirt") ∪ SKU prefix ∪ exact tag value.
|
|
13
|
+
Draft products, other tenants, and products outside the publishable key's
|
|
14
|
+
channels NEVER appear.
|
|
15
|
+
|
|
16
|
+
**Synonyms (admin-authored)** — expansion is BIDIRECTIONAL and SINGLE-LEVEL
|
|
17
|
+
(bounded — expansions never re-expand; capped at 24 terms): a query word
|
|
18
|
+
matching a row's term adds its synonyms; a query word matching one of a
|
|
19
|
+
row's synonyms adds the term + its sibling synonyms. So with the admin row
|
|
20
|
+
`term: "зехтин", synonyms: ["olive oil"]`, EITHER direction finds the
|
|
21
|
+
product. Terms are normalized lowercase.
|
|
22
|
+
|
|
23
|
+
**Pins & boosts (admin-authored)** — ordering is: (1) pins for this EXACT
|
|
24
|
+
normalized query, in position order — pins are INJECTED even without a text
|
|
25
|
+
match, but only if they pass filters + channel scope + published (the leak
|
|
26
|
+
rule applies to pins too); then (2) globally-boosted products WITHIN the
|
|
27
|
+
match set, in position order; then (3) relevance rank with a deterministic
|
|
28
|
+
tie-break. Global boosts reorder, never inject.
|
|
29
|
+
|
|
30
|
+
**Cache rule** — responses carry `calculated_price` when a pricing context
|
|
31
|
+
is given; they vary by customer group — never cache shared when a JWT was
|
|
32
|
+
present.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## GET /api/store/products/search
|
|
37
|
+
|
|
38
|
+
- **Purpose** — the search results page + autocomplete backend.
|
|
39
|
+
- **Auth** — anon: `x-client-id` required; `x-publishable-api-key` optional
|
|
40
|
+
(channel scope, products-listing semantics); Bearer JWT optional (group
|
|
41
|
+
pricing).
|
|
42
|
+
- **Request**
|
|
43
|
+
|
|
44
|
+
```jsonc
|
|
45
|
+
// query — q is REQUIRED (min 1 char), everything else optional
|
|
46
|
+
{
|
|
47
|
+
"q": "linen",
|
|
48
|
+
"collection_id": "pcol_a,pcol_b", // CSV
|
|
49
|
+
"category_id": "pcat_a", // CSV
|
|
50
|
+
"tag_id": "ptag_a", // CSV
|
|
51
|
+
"type_id": "ptyp_a", // CSV
|
|
52
|
+
"price_min": 10, // major units, on the product's cheapest base price
|
|
53
|
+
"price_max": 50,
|
|
54
|
+
"availability": "in_stock", // in_stock | out_of_stock
|
|
55
|
+
// Option filters — dynamic keys, repeatable; OR within an option, AND
|
|
56
|
+
// across options. URL-encode titles ("Length (cm)" → option.Length%20(cm)):
|
|
57
|
+
"option.Size": "S,M",
|
|
58
|
+
"currency_code": "eur", // pricing context (default price facet currency: eur)
|
|
59
|
+
"region_id": "reg_…",
|
|
60
|
+
"limit": 20, // 1–100, default 20
|
|
61
|
+
"offset": 0
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- **Response**
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{
|
|
69
|
+
"results": [ /* canonical product objects — see products.md */ ],
|
|
70
|
+
"facets": [
|
|
71
|
+
{
|
|
72
|
+
"key": "price", // "price" | "availability" | "type" |
|
|
73
|
+
// "tags" | "collection" | "options.<Title>"
|
|
74
|
+
"label": "Price",
|
|
75
|
+
"type": "range", // "range" for price, "value" otherwise
|
|
76
|
+
"buckets": [
|
|
77
|
+
{ "value": "20-40", "label": "€20 – €40", "count": 3,
|
|
78
|
+
"min": 20, "max": 40 } // min/max on range buckets only; max null = open top
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"key": "availability", "label": "Availability", "type": "value",
|
|
83
|
+
"buckets": [ { "value": "in_stock", "label": "In stock", "count": 5 } ]
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"total": 5,
|
|
87
|
+
"offset": 0,
|
|
88
|
+
"limit": 20
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Facets** follow the tenant's Settings → Search & discovery → Filters
|
|
93
|
+
config (order + enabled; defaults: price, availability, type, tags,
|
|
94
|
+
collection). **The faceting count rule:** each facet's bucket counts are
|
|
95
|
+
computed on the result set filtered by every OTHER active filter — the
|
|
96
|
+
facet's own dimension is excluded, so selecting "Size: M" never zeroes the
|
|
97
|
+
other sizes. Empty buckets are omitted; a facet with no buckets is
|
|
98
|
+
omitted (the dev tenant has no tags/types, so those facets simply don't
|
|
99
|
+
appear). Price buckets: `auto` (deterministic nice-number split of
|
|
100
|
+
observed prices, ≤10 buckets) or `fixed` admin ranges; apply one by
|
|
101
|
+
passing its `min`/`max` back as `price_min`/`price_max`. Value-facet
|
|
102
|
+
buckets carry ids as `value` (type/tags/collection) or the option value
|
|
103
|
+
string (`options.<Title>`).
|
|
104
|
+
|
|
105
|
+
- **Working curl** — seeded catalog: Linen Shirt (45), Wool Beanie (23),
|
|
106
|
+
Leather Belt (60), all in the `essentials` collection:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
SEARCH=$(curl -sf "$BASE/api/store/products/search?q=linen¤cy_code=eur" \
|
|
110
|
+
-H "x-client-id: $CLIENT_ID")
|
|
111
|
+
echo "$SEARCH" | grep -q '"results"'
|
|
112
|
+
echo "$SEARCH" | grep -q '"facets"'
|
|
113
|
+
echo "$SEARCH" | grep -q '"total"'
|
|
114
|
+
echo "$SEARCH" | grep -q '"handle":"linen-shirt"'
|
|
115
|
+
echo "$SEARCH" | grep -q '"calculated_price"'
|
|
116
|
+
# Default facet config puts price + availability on a priced, stocked catalog:
|
|
117
|
+
echo "$SEARCH" | grep -q '"key":"price"'
|
|
118
|
+
echo "$SEARCH" | grep -q '"key":"availability"'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Typo tolerance (title trigram): "linnen" still finds the Linen Shirt.
|
|
123
|
+
curl -sf "$BASE/api/store/products/search?q=linnen" -H "x-client-id: $CLIENT_ID" \
|
|
124
|
+
| grep -q '"handle":"linen-shirt"'
|
|
125
|
+
# SKU prefix match:
|
|
126
|
+
curl -sf "$BASE/api/store/products/search?q=LIN-SHIRT" -H "x-client-id: $CLIENT_ID" \
|
|
127
|
+
| grep -q '"handle":"linen-shirt"'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Option filter: only the Linen Shirt has Size S; the beanie (Color) and the
|
|
132
|
+
# belt (Length (cm)) don't match option.Size at all.
|
|
133
|
+
OPT=$(curl -sf "$BASE/api/store/products/search?q=linen&option.Size=S" \
|
|
134
|
+
-H "x-client-id: $CLIENT_ID")
|
|
135
|
+
echo "$OPT" | grep -q '"handle":"linen-shirt"'
|
|
136
|
+
# A value no variant carries → empty result set (envelope intact):
|
|
137
|
+
curl -sf "$BASE/api/store/products/search?q=linen&option.Size=XXL" \
|
|
138
|
+
-H "x-client-id: $CLIENT_ID" | grep -q '"results":\[\]'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Channel scope: the dev PUBLISHABLE_KEY is bound to the B2B channel
|
|
143
|
+
# (shirt + beanie). "leather" matches anon but returns nothing under the key.
|
|
144
|
+
curl -sf "$BASE/api/store/products/search?q=leather" -H "x-client-id: $CLIENT_ID" \
|
|
145
|
+
| grep -q '"handle":"leather-belt"'
|
|
146
|
+
curl -sf "$BASE/api/store/products/search?q=leather" \
|
|
147
|
+
-H "x-client-id: $CLIENT_ID" -H "x-publishable-api-key: $PUBLISHABLE_KEY" \
|
|
148
|
+
| grep -q '"results":\[\]'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **Errors** — 400 `validation_failed` (missing/empty `q`, bad
|
|
152
|
+
`availability`, limit > 100), 400 `missing_client_id`,
|
|
153
|
+
400 `invalid_publishable_key`, 400 `invalid_region`.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
157
|
+
"$BASE/api/store/products/search" -H "x-client-id: $CLIENT_ID")
|
|
158
|
+
test "$STATUS" = 400
|
|
159
|
+
curl -s "$BASE/api/store/products/search" -H "x-client-id: $CLIENT_ID" \
|
|
160
|
+
| grep -q '"code":"validation_failed"'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **SDK** — `searchProducts(client, query)` — pass option filters as
|
|
164
|
+
`options: { Size: ["S","M"] }`; the SDK folds them into `option.<Title>`
|
|
165
|
+
params.
|
|
166
|
+
- **Components** — search page, facet sidebar, autocomplete.
|
|
167
|
+
- **Settings** — Search & discovery: synonyms, pins (per-query) + global
|
|
168
|
+
boosts, facet order/enabled, price bucket strategy (auto/fixed).
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## GET /api/store/products/:idOrHandle/related
|
|
173
|
+
|
|
174
|
+
- **Purpose** — the PDP's product-to-product rails, BOTH of them,
|
|
175
|
+
discriminated by `kind`:
|
|
176
|
+
- `kind=related` (default) — the rail below the product. Manual admin picks
|
|
177
|
+
first (position order), then a DETERMINISTIC fallback fills to `limit`:
|
|
178
|
+
same primary collection (newest first), then most-shared-tags.
|
|
179
|
+
`auto_filled: true` when any fallback item is present.
|
|
180
|
+
- `kind=complementary` — the "goes with this" slot above the product. The
|
|
181
|
+
merchant's picks in their order and NOTHING else; `auto_filled` is always
|
|
182
|
+
false. There is no fallback by design: similarity can be computed,
|
|
183
|
+
complementarity is a merchant judgement, and a guessed upsell beside the
|
|
184
|
+
buy button is worse than an empty slot. An empty response means render
|
|
185
|
+
nothing.
|
|
186
|
+
- **Auth** — anon: `x-client-id`; optional publishable key — the anchor
|
|
187
|
+
product must be visible to the key, and scoped-away products never appear
|
|
188
|
+
as related items; optional Bearer JWT (group pricing).
|
|
189
|
+
- **Request** — query `{ kind? (related|complementary, default related),
|
|
190
|
+
limit? (1–24, default 12), currency_code?, region_id? }`. Accepts a product
|
|
191
|
+
id (`prod_…`) or handle.
|
|
192
|
+
- **Response**
|
|
193
|
+
|
|
194
|
+
```jsonc
|
|
195
|
+
{
|
|
196
|
+
"products": [ /* canonical product objects, self-excluded */ ],
|
|
197
|
+
"count": 2,
|
|
198
|
+
"auto_filled": true // at least one item came from the fallback chain
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- **Working curl** — the seeded products share the `essentials` collection,
|
|
203
|
+
so the shirt's rail fills from it:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
RELATED=$(curl -sf "$BASE/api/store/products/linen-shirt/related?currency_code=eur" \
|
|
207
|
+
-H "x-client-id: $CLIENT_ID")
|
|
208
|
+
echo "$RELATED" | grep -q '"products"'
|
|
209
|
+
echo "$RELATED" | grep -q '"auto_filled"'
|
|
210
|
+
echo "$RELATED" | grep -q '"count"'
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- **Errors** — 404 `not_found` (unknown id/handle, draft, or anchor outside
|
|
214
|
+
the key's channels), 400 `validation_failed`, 400 `invalid_region`.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# Channel scope on the ANCHOR: the belt is outside the B2B key's channels.
|
|
218
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
219
|
+
"$BASE/api/store/products/leather-belt/related" \
|
|
220
|
+
-H "x-client-id: $CLIENT_ID" -H "x-publishable-api-key: $PUBLISHABLE_KEY")
|
|
221
|
+
test "$STATUS" = 404
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- **SDK** — `listRelatedProducts(client, idOrHandle, query?)`.
|
|
225
|
+
- **Components** — PDP related rail.
|
|
226
|
+
- **Settings** — manual picks: Admin → Product → Related (drag-ordered,
|
|
227
|
+
≤24); the fallback chain needs no configuration.
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
# Store
|
|
2
|
-
|
|
3
|
-
The store's own identity: its name, its slug and its brand (what the
|
|
4
|
-
merchant set under Settings → Brand). One anonymous read, usually at
|
|
5
|
-
layout level, so a storefront titles, heads and foots itself with the
|
|
6
|
-
merchant's name and never hardcodes one. Logo URLs are public. Colors are
|
|
7
|
-
`#rrggbb`. Every field except `name` and `slug` may be null.
|
|
8
|
-
|
|
9
|
-
SDK module: `@cartbase/storefront/api/store` (from 0.8.0; on 0.7.0 call
|
|
10
|
-
`client.request("/api/store/store")` with the same shape).
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## GET /api/store/store
|
|
15
|
-
|
|
16
|
-
- **Purpose** — read the store's id, name, slug and brand.
|
|
17
|
-
- **Auth** — anon: the store's publishable key (`x-publishable-api-key`).
|
|
18
|
-
- **Request** — `GET /api/store/store`
|
|
19
|
-
- **Response 200**
|
|
20
|
-
|
|
21
|
-
```jsonc
|
|
22
|
-
{
|
|
23
|
-
"store": {
|
|
24
|
-
"id": "1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd", // the store's public id (what PlatformInit mounts)
|
|
25
|
-
"name": "Demo Store",
|
|
26
|
-
"slug": "demo-store",
|
|
27
|
-
"brand": {
|
|
28
|
-
"logo_url": "https://…/brand/…/logo.png", // or null
|
|
29
|
-
"logo_square_url": null,
|
|
30
|
-
"color_primary": "#111111", // or null
|
|
31
|
-
"color_secondary": null,
|
|
32
|
-
"slogan": null
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
- **Errors** — `404 store_not_found` when the key names no live store.
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
import { getStore } from "@cartbase/storefront/api/store"
|
|
42
|
-
|
|
43
|
-
export async function generateMetadata() {
|
|
44
|
-
const { store } = await getStore(await getServerClient())
|
|
45
|
-
return createStorefrontMetadata({ title: store.name, description: store.brand.slogan ?? undefined })
|
|
46
|
-
}
|
|
47
|
-
```
|
|
1
|
+
# Store
|
|
2
|
+
|
|
3
|
+
The store's own identity: its name, its slug and its brand (what the
|
|
4
|
+
merchant set under Settings → Brand). One anonymous read, usually at
|
|
5
|
+
layout level, so a storefront titles, heads and foots itself with the
|
|
6
|
+
merchant's name and never hardcodes one. Logo URLs are public. Colors are
|
|
7
|
+
`#rrggbb`. Every field except `name` and `slug` may be null.
|
|
8
|
+
|
|
9
|
+
SDK module: `@cartbase/storefront/api/store` (from 0.8.0; on 0.7.0 call
|
|
10
|
+
`client.request("/api/store/store")` with the same shape).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## GET /api/store/store
|
|
15
|
+
|
|
16
|
+
- **Purpose** — read the store's id, name, slug and brand.
|
|
17
|
+
- **Auth** — anon: the store's publishable key (`x-publishable-api-key`).
|
|
18
|
+
- **Request** — `GET /api/store/store`
|
|
19
|
+
- **Response 200**
|
|
20
|
+
|
|
21
|
+
```jsonc
|
|
22
|
+
{
|
|
23
|
+
"store": {
|
|
24
|
+
"id": "1e7a4c02-9b31-4f7e-8d2a-5c6f90ab12cd", // the store's public id (what PlatformInit mounts)
|
|
25
|
+
"name": "Demo Store",
|
|
26
|
+
"slug": "demo-store",
|
|
27
|
+
"brand": {
|
|
28
|
+
"logo_url": "https://…/brand/…/logo.png", // or null
|
|
29
|
+
"logo_square_url": null,
|
|
30
|
+
"color_primary": "#111111", // or null
|
|
31
|
+
"color_secondary": null,
|
|
32
|
+
"slogan": null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **Errors** — `404 store_not_found` when the key names no live store.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { getStore } from "@cartbase/storefront/api/store"
|
|
42
|
+
|
|
43
|
+
export async function generateMetadata() {
|
|
44
|
+
const { store } = await getStore(await getServerClient())
|
|
45
|
+
return createStorefrontMetadata({ title: store.name, description: store.brand.slogan ?? undefined })
|
|
46
|
+
}
|
|
47
|
+
```
|