create-cartbase 0.1.14 → 0.1.16
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/template/app/docs/collections.md +167 -167
- package/template/app/docs/components.md +1294 -1246
- package/template/app/docs/customers.md +3 -4
- package/template/app/docs/products.md +316 -307
- package/template/app/next.config.ts +6 -4
- package/template/app/package.json +1 -1
- package/template/app/src/app/products/[handle]/page.tsx +9 -1
package/package.json
CHANGED
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
# Collections
|
|
2
|
-
|
|
3
|
-
Curated product groupings (manual or smart). The membership listing —
|
|
4
|
-
`/collections/:id/products` — is the collection page's data source: it reads
|
|
5
|
-
the membership JOIN (multi-collection products appear in every collection
|
|
6
|
-
they belong to), honors the collection's `default_sort`, and accepts a
|
|
7
|
-
per-request `order` override. Money is EUR decimal major units.
|
|
8
|
-
|
|
9
|
-
SDK module: `@cartbase/storefront/api/collections`.
|
|
10
|
-
|
|
11
|
-
**Channel scope (Shopify publish-to-channel semantics):** a collection with
|
|
12
|
-
sales-channel links is visible ONLY on those channels; a collection with no
|
|
13
|
-
links is visible everywhere. Pass your channel as `sales_channel_id` — the
|
|
14
|
-
list excludes scoped-away collections, and the membership listing 404s them.
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## GET /api/store/collections
|
|
19
|
-
|
|
20
|
-
- **Purpose** — list collections (navigation, collection index pages).
|
|
21
|
-
- **Auth** — anon: `x-client-id` required.
|
|
22
|
-
- **Request**
|
|
23
|
-
|
|
24
|
-
```jsonc
|
|
25
|
-
// query (all optional)
|
|
26
|
-
{
|
|
27
|
-
"q": "essen", // case-insensitive substring on title
|
|
28
|
-
"handle": "essentials", // exact — THE handle lookup (no /:handle route)
|
|
29
|
-
"sales_channel_id": "sc_…", // channel scope (see above)
|
|
30
|
-
"limit": 50, // 1–200, default 50
|
|
31
|
-
"offset": 0
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
- **Response** — `{ collections, count, offset, limit }`, ordered by title:
|
|
36
|
-
|
|
37
|
-
```jsonc
|
|
38
|
-
{
|
|
39
|
-
"collections": [
|
|
40
|
-
{
|
|
41
|
-
"id": "pcol_01tst00000000000000000001",
|
|
42
|
-
"title": "Essentials",
|
|
43
|
-
"handle": "essentials",
|
|
44
|
-
"type": "manual", // "manual" | "smart"
|
|
45
|
-
"description": null,
|
|
46
|
-
"image_url": null,
|
|
47
|
-
"default_sort": "manual", // used by /products when no order override
|
|
48
|
-
"conditions": [], // smart-collection rules (admin-authored)
|
|
49
|
-
"match": "all", // smart matching: "all" | "any"
|
|
50
|
-
"seo_title": null, // null = fall back to title
|
|
51
|
-
"seo_description": null, // null = fall back to description
|
|
52
|
-
"metadata": null,
|
|
53
|
-
"created_at": "2026-07-01T00:00:00.000Z",
|
|
54
|
-
"updated_at": "2026-07-01T00:00:00.000Z"
|
|
55
|
-
}
|
|
56
|
-
],
|
|
57
|
-
"count": 1, "offset": 0, "limit": 50
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
- **Working curl** — the seeded catalog carries the `essentials` collection:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
COLLECTIONS=$(curl -sf "$BASE/api/store/collections?handle=essentials" \
|
|
65
|
-
-H "x-client-id: $CLIENT_ID")
|
|
66
|
-
echo "$COLLECTIONS" | grep -q '"collections"'
|
|
67
|
-
echo "$COLLECTIONS" | grep -q '"handle":"essentials"'
|
|
68
|
-
COL_ID=$(echo "$COLLECTIONS" | grep -o '"id":"pcol_[^"]*"' | head -1 | cut -d'"' -f4)
|
|
69
|
-
test -n "$COL_ID"
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
- **Errors** — 400 `missing_client_id`, 400 `validation_failed`.
|
|
73
|
-
- **SDK** — `listCollections(client, query?)`.
|
|
74
|
-
- **Components** — navigation, collection index grid.
|
|
75
|
-
- **Settings** — collection channel links; smart-collection conditions
|
|
76
|
-
(membership recomputes on rule/product change).
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## GET /api/store/collections/:id
|
|
81
|
-
|
|
82
|
-
- **Purpose** — retrieve one collection (header/SEO block of a collection
|
|
83
|
-
page). By-handle lookup goes through the list (`?handle=`).
|
|
84
|
-
- **Auth** — anon: `x-client-id` required.
|
|
85
|
-
- **Request** — no query. NOTE (code truth): the single read takes no
|
|
86
|
-
`sales_channel_id` — channel scope applies to the list and the membership
|
|
87
|
-
listing, not here.
|
|
88
|
-
- **Response** — `{ "collection": { ...same shape as list rows... } }`
|
|
89
|
-
- **Working curl**
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
COLLECTION=$(curl -sf "$BASE/api/store/collections/$COL_ID" -H "x-client-id: $CLIENT_ID")
|
|
93
|
-
echo "$COLLECTION" | grep -q '"collection"'
|
|
94
|
-
echo "$COLLECTION" | grep -q '"default_sort"'
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
- **Errors** — 404 `not_found`.
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
101
|
-
"$BASE/api/store/collections/pcol_doesnotexist$RUN" -H "x-client-id: $CLIENT_ID")
|
|
102
|
-
test "$STATUS" = 404
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
- **SDK** — `retrieveCollection(client, collectionId)`.
|
|
106
|
-
- **Components** — collection page header.
|
|
107
|
-
- **Settings** — SEO overrides.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## GET /api/store/collections/:id/products
|
|
112
|
-
|
|
113
|
-
- **Purpose** — the collection page's product grid: membership join,
|
|
114
|
-
published products only, ordered by the collection's `default_sort` with
|
|
115
|
-
an optional `order` override.
|
|
116
|
-
- **Auth** — anon: `x-client-id`; optional Bearer JWT (group pricing).
|
|
117
|
-
- **Request**
|
|
118
|
-
|
|
119
|
-
```jsonc
|
|
120
|
-
// query (all optional)
|
|
121
|
-
{
|
|
122
|
-
"order": "price_asc", // override: manual | title_asc | title_desc |
|
|
123
|
-
// price_asc | price_desc | newest | oldest |
|
|
124
|
-
// best_selling (90-day aggregate).
|
|
125
|
-
// Unknown values are IGNORED (default_sort used).
|
|
126
|
-
"sales_channel_id": "sc_…", // a collection scoped to OTHER channels 404s
|
|
127
|
-
"currency_code": "eur", // pricing context → calculated_price
|
|
128
|
-
"region_id": "reg_…",
|
|
129
|
-
"limit": 50, // 1–100, default 50
|
|
130
|
-
"offset": 0
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
- **Response** — `{ products, count, offset, limit }` — products carry the
|
|
135
|
-
FULL canonical product shape (see products.md), incl. `calculated_price`
|
|
136
|
-
when a pricing context is given. `count` is the visible membership size.
|
|
137
|
-
- **Working curl** — seeded membership
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
MEMBERS=$(curl -sf "$BASE/api/store/collections/$COL_ID/products?currency_code=eur" \
|
|
142
|
-
-H "x-client-id: $CLIENT_ID")
|
|
143
|
-
echo "$MEMBERS" | grep -q '"products"'
|
|
144
|
-
echo "$MEMBERS" | grep -q '"handle":"linen-shirt"'
|
|
145
|
-
echo "$MEMBERS" | grep -q '"calculated_price"'
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
# Sort override: price_asc puts the Wool Beanie (EUR 23) before the Linen
|
|
150
|
-
# Shirt (EUR 45) — asserted as RELATIVE order so unrelated rows can't break it.
|
|
151
|
-
curl -sf "$BASE/api/store/collections/$COL_ID/products?order=price_asc" \
|
|
152
|
-
-H "x-client-id: $CLIENT_ID" | node -e "
|
|
153
|
-
const c=[];process.stdin.on('data',d=>c.push(d)).on('end',()=>{
|
|
154
|
-
const j=JSON.parse(Buffer.concat(c));
|
|
155
|
-
const h=j.products.map(p=>p.handle);
|
|
156
|
-
const a=h.indexOf('wool-beanie'), b=h.indexOf('linen-shirt');
|
|
157
|
-
if(a<0||b<0||a>b){console.error('price_asc order wrong: '+h.join(','));process.exit(1)}
|
|
158
|
-
})"
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
- **Errors** — 404 `not_found` (unknown collection, or scoped away from the
|
|
162
|
-
given `sales_channel_id`), 400 `validation_failed`, 400 `invalid_region`.
|
|
163
|
-
- **SDK** — `listCollectionProducts(client, collectionId, query?)`.
|
|
164
|
-
- **Components** — product card grid + sort dropdown (emit the `order`
|
|
165
|
-
values above).
|
|
166
|
-
- **Settings** — collection `default_sort` + manual position order
|
|
167
|
-
(drag-reorder in admin); price lists; channel links.
|
|
1
|
+
# Collections
|
|
2
|
+
|
|
3
|
+
Curated product groupings (manual or smart). The membership listing —
|
|
4
|
+
`/collections/:id/products` — is the collection page's data source: it reads
|
|
5
|
+
the membership JOIN (multi-collection products appear in every collection
|
|
6
|
+
they belong to), honors the collection's `default_sort`, and accepts a
|
|
7
|
+
per-request `order` override. Money is EUR decimal major units.
|
|
8
|
+
|
|
9
|
+
SDK module: `@cartbase/storefront/api/collections`.
|
|
10
|
+
|
|
11
|
+
**Channel scope (Shopify publish-to-channel semantics):** a collection with
|
|
12
|
+
sales-channel links is visible ONLY on those channels; a collection with no
|
|
13
|
+
links is visible everywhere. Pass your channel as `sales_channel_id` — the
|
|
14
|
+
list excludes scoped-away collections, and the membership listing 404s them.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## GET /api/store/collections
|
|
19
|
+
|
|
20
|
+
- **Purpose** — list collections (navigation, collection index pages).
|
|
21
|
+
- **Auth** — anon: `x-client-id` required.
|
|
22
|
+
- **Request**
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
// query (all optional)
|
|
26
|
+
{
|
|
27
|
+
"q": "essen", // case-insensitive substring on title
|
|
28
|
+
"handle": "essentials", // exact — THE handle lookup (no /:handle route)
|
|
29
|
+
"sales_channel_id": "sc_…", // channel scope (see above)
|
|
30
|
+
"limit": 50, // 1–200, default 50
|
|
31
|
+
"offset": 0
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- **Response** — `{ collections, count, offset, limit }`, ordered by title:
|
|
36
|
+
|
|
37
|
+
```jsonc
|
|
38
|
+
{
|
|
39
|
+
"collections": [
|
|
40
|
+
{
|
|
41
|
+
"id": "pcol_01tst00000000000000000001",
|
|
42
|
+
"title": "Essentials",
|
|
43
|
+
"handle": "essentials",
|
|
44
|
+
"type": "manual", // "manual" | "smart"
|
|
45
|
+
"description": null,
|
|
46
|
+
"image_url": null,
|
|
47
|
+
"default_sort": "manual", // used by /products when no order override
|
|
48
|
+
"conditions": [], // smart-collection rules (admin-authored)
|
|
49
|
+
"match": "all", // smart matching: "all" | "any"
|
|
50
|
+
"seo_title": null, // null = fall back to title
|
|
51
|
+
"seo_description": null, // null = fall back to description
|
|
52
|
+
"metadata": null,
|
|
53
|
+
"created_at": "2026-07-01T00:00:00.000Z",
|
|
54
|
+
"updated_at": "2026-07-01T00:00:00.000Z"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"count": 1, "offset": 0, "limit": 50
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Working curl** — the seeded catalog carries the `essentials` collection:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
COLLECTIONS=$(curl -sf "$BASE/api/store/collections?handle=essentials" \
|
|
65
|
+
-H "x-client-id: $CLIENT_ID")
|
|
66
|
+
echo "$COLLECTIONS" | grep -q '"collections"'
|
|
67
|
+
echo "$COLLECTIONS" | grep -q '"handle":"essentials"'
|
|
68
|
+
COL_ID=$(echo "$COLLECTIONS" | grep -o '"id":"pcol_[^"]*"' | head -1 | cut -d'"' -f4)
|
|
69
|
+
test -n "$COL_ID"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **Errors** — 400 `missing_client_id`, 400 `validation_failed`.
|
|
73
|
+
- **SDK** — `listCollections(client, query?)`.
|
|
74
|
+
- **Components** — navigation, collection index grid.
|
|
75
|
+
- **Settings** — collection channel links; smart-collection conditions
|
|
76
|
+
(membership recomputes on rule/product change).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## GET /api/store/collections/:id
|
|
81
|
+
|
|
82
|
+
- **Purpose** — retrieve one collection (header/SEO block of a collection
|
|
83
|
+
page). By-handle lookup goes through the list (`?handle=`).
|
|
84
|
+
- **Auth** — anon: `x-client-id` required.
|
|
85
|
+
- **Request** — no query. NOTE (code truth): the single read takes no
|
|
86
|
+
`sales_channel_id` — channel scope applies to the list and the membership
|
|
87
|
+
listing, not here.
|
|
88
|
+
- **Response** — `{ "collection": { ...same shape as list rows... } }`
|
|
89
|
+
- **Working curl**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
COLLECTION=$(curl -sf "$BASE/api/store/collections/$COL_ID" -H "x-client-id: $CLIENT_ID")
|
|
93
|
+
echo "$COLLECTION" | grep -q '"collection"'
|
|
94
|
+
echo "$COLLECTION" | grep -q '"default_sort"'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- **Errors** — 404 `not_found`.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
101
|
+
"$BASE/api/store/collections/pcol_doesnotexist$RUN" -H "x-client-id: $CLIENT_ID")
|
|
102
|
+
test "$STATUS" = 404
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **SDK** — `retrieveCollection(client, collectionId)`.
|
|
106
|
+
- **Components** — collection page header.
|
|
107
|
+
- **Settings** — SEO overrides.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## GET /api/store/collections/:id/products
|
|
112
|
+
|
|
113
|
+
- **Purpose** — the collection page's product grid: membership join,
|
|
114
|
+
published products only, ordered by the collection's `default_sort` with
|
|
115
|
+
an optional `order` override.
|
|
116
|
+
- **Auth** — anon: `x-client-id`; optional Bearer JWT (group pricing).
|
|
117
|
+
- **Request**
|
|
118
|
+
|
|
119
|
+
```jsonc
|
|
120
|
+
// query (all optional)
|
|
121
|
+
{
|
|
122
|
+
"order": "price_asc", // override: manual | title_asc | title_desc |
|
|
123
|
+
// price_asc | price_desc | newest | oldest |
|
|
124
|
+
// best_selling (90-day aggregate).
|
|
125
|
+
// Unknown values are IGNORED (default_sort used).
|
|
126
|
+
"sales_channel_id": "sc_…", // a collection scoped to OTHER channels 404s
|
|
127
|
+
"currency_code": "eur", // pricing context → calculated_price
|
|
128
|
+
"region_id": "reg_…",
|
|
129
|
+
"limit": 50, // 1–100, default 50
|
|
130
|
+
"offset": 0
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- **Response** — `{ products, count, offset, limit }` — products carry the
|
|
135
|
+
FULL canonical product shape (see products.md), incl. `calculated_price`
|
|
136
|
+
when a pricing context is given. `count` is the visible membership size.
|
|
137
|
+
- **Working curl** — the seeded membership contains the three fixture
|
|
138
|
+
products:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
MEMBERS=$(curl -sf "$BASE/api/store/collections/$COL_ID/products?currency_code=eur" \
|
|
142
|
+
-H "x-client-id: $CLIENT_ID")
|
|
143
|
+
echo "$MEMBERS" | grep -q '"products"'
|
|
144
|
+
echo "$MEMBERS" | grep -q '"handle":"linen-shirt"'
|
|
145
|
+
echo "$MEMBERS" | grep -q '"calculated_price"'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Sort override: price_asc puts the Wool Beanie (EUR 23) before the Linen
|
|
150
|
+
# Shirt (EUR 45) — asserted as RELATIVE order so unrelated rows can't break it.
|
|
151
|
+
curl -sf "$BASE/api/store/collections/$COL_ID/products?order=price_asc" \
|
|
152
|
+
-H "x-client-id: $CLIENT_ID" | node -e "
|
|
153
|
+
const c=[];process.stdin.on('data',d=>c.push(d)).on('end',()=>{
|
|
154
|
+
const j=JSON.parse(Buffer.concat(c));
|
|
155
|
+
const h=j.products.map(p=>p.handle);
|
|
156
|
+
const a=h.indexOf('wool-beanie'), b=h.indexOf('linen-shirt');
|
|
157
|
+
if(a<0||b<0||a>b){console.error('price_asc order wrong: '+h.join(','));process.exit(1)}
|
|
158
|
+
})"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- **Errors** — 404 `not_found` (unknown collection, or scoped away from the
|
|
162
|
+
given `sales_channel_id`), 400 `validation_failed`, 400 `invalid_region`.
|
|
163
|
+
- **SDK** — `listCollectionProducts(client, collectionId, query?)`.
|
|
164
|
+
- **Components** — product card grid + sort dropdown (emit the `order`
|
|
165
|
+
values above).
|
|
166
|
+
- **Settings** — collection `default_sort` + manual position order
|
|
167
|
+
(drag-reorder in admin); price lists; channel links.
|