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.
Files changed (34) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +25 -25
  3. package/dist/index.js +20 -20
  4. package/package.json +1 -1
  5. package/template/app/docs/auth.md +105 -105
  6. package/template/app/docs/carts.md +376 -376
  7. package/template/app/docs/categories.md +194 -194
  8. package/template/app/docs/checkout.md +714 -714
  9. package/template/app/docs/components.md +55 -11
  10. package/template/app/docs/consent.md +91 -91
  11. package/template/app/docs/deploy.md +197 -197
  12. package/template/app/docs/gift-cards.md +153 -153
  13. package/template/app/docs/metaobjects.md +126 -126
  14. package/template/app/docs/orders.md +221 -221
  15. package/template/app/docs/products.md +51 -2
  16. package/template/app/docs/regions.md +269 -269
  17. package/template/app/docs/reviews.md +223 -223
  18. package/template/app/docs/search.md +227 -227
  19. package/template/app/docs/store.md +47 -47
  20. package/template/app/docs/subscriptions.md +148 -148
  21. package/template/app/docs/variables.md +315 -315
  22. package/template/app/package.json +1 -1
  23. package/template/app/postcss.config.cjs +11 -11
  24. package/template/app/src/app/checkout/checkout-page-client.tsx +73 -73
  25. package/template/app/src/app/checkout/page.tsx +48 -48
  26. package/template/app/src/app/globals.css +26 -26
  27. package/template/app/src/app/page.tsx +28 -28
  28. package/template/app/src/app/products/[handle]/page.tsx +87 -87
  29. package/template/app/src/app/providers.tsx +68 -64
  30. package/template/app/src/app/search/page.tsx +23 -23
  31. package/template/app/src/lib/browser-client.ts +35 -35
  32. package/template/app/src/lib/config.ts +41 -41
  33. package/template/app/src/lib/server-client.ts +25 -25
  34. 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&currency_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&currency_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
+ ```