@spree/docs 0.1.183 → 0.1.184
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/dist/developer/core-concepts/catalogs.md +140 -0
- package/dist/developer/core-concepts/commissions.md +253 -0
- package/dist/developer/core-concepts/companies.md +240 -0
- package/dist/developer/core-concepts/media.md +120 -7
- package/dist/developer/core-concepts/products.md +156 -43
- package/dist/developer/core-concepts/sellers.md +210 -0
- package/dist/developer/core-concepts/staff-roles.md +56 -23
- package/dist/developer/core-concepts/taxes.md +2 -2
- package/package.json +1 -1
- package/dist/developer/core-concepts/companies-and-catalogs.md +0 -81
|
@@ -1,14 +1,88 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Media
|
|
3
3
|
sidebarTitle: "Media"
|
|
4
|
-
description:
|
|
4
|
+
description: The store-wide media library — uploading once and reusing everywhere, images and video, focal points, and how files are resized and served.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
Spree handles
|
|
9
|
+
Spree handles uploading, processing and serving the images and video a store shows. Files are converted to WebP and preprocessed into several sizes, so a listing page isn't downloading full-resolution photographs.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Files live in a **store-wide media library**. A file is uploaded once and can then be placed wherever it's needed — on a product, on a category, inside a description — without being uploaded again.
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart LR
|
|
15
|
+
Upload["Upload once"] --> Library["Media library"]
|
|
16
|
+
Library --> P["On a product"]
|
|
17
|
+
Library --> C["On a category"]
|
|
18
|
+
Library --> D["Inside a description"]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The media library
|
|
22
|
+
|
|
23
|
+
Every file in a store is in the library, whether or not it's currently placed on anything. That means a file can be uploaded ahead of time, browsed, searched, and reused.
|
|
24
|
+
|
|
25
|
+
```typescript Admin SDK
|
|
26
|
+
// Browse everything in the store
|
|
27
|
+
const { data: media } = await adminClient.media.list()
|
|
28
|
+
|
|
29
|
+
// Where is this file being used?
|
|
30
|
+
const { data: usage } = await adminClient.media.usage('med_xxx')
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Field | What it tells you |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `attached` | Whether the file is placed on anything yet |
|
|
36
|
+
| `filename`, `content_type`, `byte_size` | What the file actually is |
|
|
37
|
+
| `viewable_type` / `viewable_id` | What it's placed on, if anything |
|
|
38
|
+
| `media_type` | `image`, `video` or `external_video` |
|
|
39
|
+
| `alt` | Alt text, for accessibility and SEO |
|
|
40
|
+
| `position` | Order within a gallery |
|
|
41
|
+
|
|
42
|
+
### Reuse shares the file, not the record
|
|
43
|
+
|
|
44
|
+
Placing a library file on a product creates a **new media row that shares the same underlying file**:
|
|
45
|
+
|
|
46
|
+
```typescript Admin SDK
|
|
47
|
+
await adminClient.products.media.create('prod_xxx', {
|
|
48
|
+
source_media_id: 'med_xxx',
|
|
49
|
+
alt: 'Worn over a navy jumper',
|
|
50
|
+
position: 2,
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The copy gets its own alt text, its own position, and its own variant links — because the same photograph might be the third image on one product and the hero on another, with different alt text describing what matters in each context. What it doesn't get is a second copy of the bytes.
|
|
55
|
+
|
|
56
|
+
> **NOTE:** This is why there's no single "shared asset" record to manage. Each placement is independently editable, while storage is shared. Detaching a photo from one product has no effect on any other.
|
|
57
|
+
|
|
58
|
+
### Deleting, safely
|
|
59
|
+
|
|
60
|
+
A file in use can't be deleted by accident:
|
|
61
|
+
|
|
62
|
+
```typescript Admin SDK
|
|
63
|
+
// Returns 422 with the list of places it's used
|
|
64
|
+
await adminClient.media.delete('med_xxx')
|
|
65
|
+
|
|
66
|
+
// Remove it from everywhere, then delete
|
|
67
|
+
await adminClient.media.delete('med_xxx', { detach: true })
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The dashboard shows you the usage list and asks before doing the second one. Check `usage` before offering a delete in your own tooling.
|
|
71
|
+
|
|
72
|
+
## What can carry media
|
|
73
|
+
|
|
74
|
+
| Owner | Typical use |
|
|
75
|
+
|---|---|
|
|
76
|
+
| **Product** | The main gallery |
|
|
77
|
+
| **Variant** | Images specific to one colour or size |
|
|
78
|
+
| **Category** | A banner or tile for a navigation page |
|
|
79
|
+
| **Collection** | The same, for a merchandising group |
|
|
80
|
+
|
|
81
|
+
Categories and collections also have simple `image` and `square_image` slots. Setting one places the file; clearing it **removes the placement but keeps the file in the library**, so nothing is destroyed by tidying up a page.
|
|
82
|
+
|
|
83
|
+
Sellers and stores keep their branding as plain attachments — a logo isn't merchandising, and it doesn't belong in a library people browse for product photos.
|
|
84
|
+
|
|
85
|
+
## Product media
|
|
12
86
|
|
|
13
87
|
A media record carries:
|
|
14
88
|
|
|
@@ -19,9 +93,9 @@ A media record carries:
|
|
|
19
93
|
- **Preprocessed named variants** for fast delivery
|
|
20
94
|
- **`variant_ids`** — which product variants the media represents. An empty array means it represents the product as a whole.
|
|
21
95
|
|
|
22
|
-
### Product-level
|
|
96
|
+
### Product-level gallery
|
|
23
97
|
|
|
24
|
-
|
|
98
|
+
Media belongs to the **product**, and any subset of its variants can reference the same file through `variant_ids` — so one photograph can represent three colourways without being uploaded three times.
|
|
25
99
|
|
|
26
100
|
#### Uploading a product-level image
|
|
27
101
|
|
|
@@ -323,9 +397,48 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,vari
|
|
|
323
397
|
| `poster_url` | string \| null | Still frame for a video |
|
|
324
398
|
| `original_url` | string \| null | Full-size image URL (inline disposition) |
|
|
325
399
|
| `mini_url` ... `xlarge_url` | string \| null | Named variant URLs |
|
|
326
|
-
| `download_url` | string \| null | Same blob as `original_url` but with `Content-Disposition: attachment`. Admin API only. |
|
|
327
400
|
|
|
328
|
-
|
|
401
|
+
The Admin API adds the fields the library needs:
|
|
402
|
+
|
|
403
|
+
| Field | Type | Description |
|
|
404
|
+
|---|---|---|
|
|
405
|
+
| `attached` | boolean | Whether the file is placed on anything, or sitting unused in the library |
|
|
406
|
+
| `viewable_type` / `viewable_id` | string \| null | What it's placed on — `product`, `variant`, `category`, `collection` |
|
|
407
|
+
| `filename`, `content_type`, `byte_size` | | What the file actually is |
|
|
408
|
+
| `embed_url` | string \| null | The rendition used inside rich text — sized to fit, not cropped |
|
|
409
|
+
| `signed_id` | string \| null | Lets a plain attachment field adopt this file without re-uploading |
|
|
410
|
+
| `download_url` | string \| null | The same file with `Content-Disposition: attachment` |
|
|
411
|
+
|
|
412
|
+
> **NOTE:** Two behaviours to expect when rendering a gallery:
|
|
413
|
+
>
|
|
414
|
+
> **`original_url` is null for any playable video** — there's no still image to size. Use `poster_url`.
|
|
415
|
+
>
|
|
416
|
+
> **A video's sized URLs come from its poster.** If a video has a poster, every `*_url` resolves from that frame, so a grid of mixed images and video renders uniformly.
|
|
417
|
+
|
|
418
|
+
## Images in descriptions
|
|
419
|
+
|
|
420
|
+
Rich text fields — a product description, a category's copy — can carry embedded images. In the dashboard, the editor opens the media library so a merchant picks an existing file or uploads a new one.
|
|
421
|
+
|
|
422
|
+
Embedded images use their own rendition, sized to fit rather than cropped to a square. A size chart or a diagram keeps its proportions, where a gallery thumbnail would have had its edges cut off.
|
|
423
|
+
|
|
424
|
+
> **WARNING:** An embedded image is a plain image URL in the HTML — nothing records which description uses which file.
|
|
425
|
+
>
|
|
426
|
+
> Spree's usage check does its best by searching descriptions for the file, but it can't be exhaustive. Deleting a library file can leave a broken image in a description, so treat the usage list as a warning rather than proof a file is unused.
|
|
427
|
+
|
|
428
|
+
## Who can manage media
|
|
429
|
+
|
|
430
|
+
Media is its own permission, separate from products:
|
|
431
|
+
|
|
432
|
+
| Action | Requires |
|
|
433
|
+
|---|---|
|
|
434
|
+
| A product's own gallery — add, reorder, remove | Permission to edit **products** |
|
|
435
|
+
| Browsing the library, checking usage, deleting a file outright | **Media** permission |
|
|
436
|
+
|
|
437
|
+
The reasoning: reaching a file through a product you can already edit isn't the same as enumerating every file in the store. Someone who manages one product's photos shouldn't automatically be able to browse — or delete — everything the business has ever uploaded.
|
|
438
|
+
|
|
439
|
+
Note the asymmetry that follows. Removing an image from a product removes the *placement* and needs only product permission; deleting the file everywhere is a library action and needs the media key.
|
|
440
|
+
|
|
441
|
+
## Image processing
|
|
329
442
|
|
|
330
443
|
Spree uses [libvips](https://www.libvips.org/) for image processing. Images are automatically:
|
|
331
444
|
|
|
@@ -5,11 +5,13 @@ description: How Spree models products, variants, option types, images, prices,
|
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
A product
|
|
8
|
+
A product is the listing a customer browses. A **variant** is the thing they actually buy.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
That split runs through everything on this page. The product holds what's shared — name, description, images, which categories it's filed under. Each variant holds what differs — its SKU, its price, its stock. A T-shirt is one product; small navy is a variant.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
**Every product has at least one variant**, even when there's nothing to choose. A book with no size or colour still has a single variant carrying its SKU, price and stock; you just never render a picker.
|
|
13
|
+
|
|
14
|
+
> **INFO:** Product names, descriptions, slugs and SEO fields are [translatable](translations.md#resource-translations).
|
|
13
15
|
|
|
14
16
|
```mermaid
|
|
15
17
|
erDiagram
|
|
@@ -59,21 +61,26 @@ erDiagram
|
|
|
59
61
|
## Product Attributes
|
|
60
62
|
|
|
61
63
|
| Attribute | Description | Translatable |
|
|
62
|
-
|
|
64
|
+
|---|---|:---:|
|
|
63
65
|
| `name` | Product name | Yes |
|
|
64
|
-
| `description` |
|
|
65
|
-
| `slug` | URL
|
|
66
|
-
| `status` | `draft`, `active
|
|
67
|
-
| `available_on` |
|
|
68
|
-
| `discontinue_on` |
|
|
69
|
-
| `meta_title` |
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
66
|
+
| `description` / `description_html` | Description as plain text and as formatted HTML | Yes |
|
|
67
|
+
| `slug` | URL identifier, e.g. `spree-tote` | Yes |
|
|
68
|
+
| `status` | `draft`, `active` or `archived`. A marketplace adds `proposed` and `rejected` — see [Seller submissions](#seller-submissions) | No |
|
|
69
|
+
| `available_on` | When it goes on sale | No |
|
|
70
|
+
| `discontinue_on` | When it comes off | No |
|
|
71
|
+
| `meta_title` / `meta_description` / `meta_keywords` | SEO fields | Yes |
|
|
72
|
+
| `purchasable` | Whether it can be added to a cart | No |
|
|
73
|
+
| `in_stock` | Whether any variant has stock | No |
|
|
74
|
+
| `backorderable` | Whether it can be ordered while out of stock | No |
|
|
75
|
+
| `preorder` / `preorder_ships_at` | Whether it's sold ahead of availability, and when it ships | No |
|
|
76
|
+
| `available` | Whether it's on sale right now, by date and status | No |
|
|
77
|
+
| `price` / `original_price` | The [default variant's](#the-default-variant) price, and its compare-at price | No |
|
|
78
|
+
| `default_variant_id` | Which variant represents the product | No |
|
|
79
|
+
| `variant_count` | How many variants it has | No |
|
|
80
|
+
| `thumbnail_url` | First image — always returned, no expand needed | No |
|
|
81
|
+
| `tags` | Tags, for filtering | No |
|
|
82
|
+
|
|
83
|
+
The Admin API adds the operational fields on top: `product_type_id`, `delivery_profile_id`, `tax_category_id`, `seller_id`, `metadata`, `created_at` / `updated_at` / `deleted_at`.
|
|
77
84
|
|
|
78
85
|
## Listing Products
|
|
79
86
|
|
|
@@ -160,18 +167,20 @@ Pass `expand` to include related resources in a single response — see [expand
|
|
|
160
167
|
|
|
161
168
|
The examples above use the **Store API** (publishable key, read-only, customer-facing). To **create and manage** products, use the [Admin API](../../api-reference/admin-api/introduction.md) — via the [Admin SDK](../sdk/admin/quickstart.md) or the [Spree CLI](../cli/admin-api.md).
|
|
162
169
|
|
|
163
|
-
A product's purchasable attributes (SKU, prices, stock) live on its **variants**, which you can create inline
|
|
170
|
+
A product's purchasable attributes (SKU, prices, stock) live on its **variants**, which you can create inline.
|
|
171
|
+
|
|
172
|
+
For a product with options, send the variants:
|
|
164
173
|
|
|
165
174
|
|
|
166
175
|
```typescript Admin SDK
|
|
167
176
|
import { createAdminClient } from '@spree/admin-sdk'
|
|
168
177
|
|
|
169
|
-
const
|
|
178
|
+
const adminClient = createAdminClient({
|
|
170
179
|
baseUrl: 'https://store.example.com',
|
|
171
180
|
secretKey: 'sk_xxx',
|
|
172
181
|
})
|
|
173
182
|
|
|
174
|
-
const product = await
|
|
183
|
+
const product = await adminClient.products.create({
|
|
175
184
|
name: 'Premium T-Shirt',
|
|
176
185
|
description: 'Soft, organic cotton.',
|
|
177
186
|
status: 'active',
|
|
@@ -183,7 +192,7 @@ const product = await client.products.create({
|
|
|
183
192
|
{ name: 'color', value: 'navy' },
|
|
184
193
|
],
|
|
185
194
|
prices: [{ currency: 'USD', amount: '29.99' }],
|
|
186
|
-
|
|
195
|
+
stock_levels: [{ stock_location_id: 'sloc_xxx', count_on_hand: 50 }],
|
|
187
196
|
},
|
|
188
197
|
],
|
|
189
198
|
})
|
|
@@ -202,6 +211,18 @@ spree api post /products -d '{
|
|
|
202
211
|
```
|
|
203
212
|
|
|
204
213
|
|
|
214
|
+
For something with no options at all, send prices directly and skip the variants array — Spree forwards them to the product's single variant:
|
|
215
|
+
|
|
216
|
+
```typescript Admin SDK
|
|
217
|
+
await adminClient.products.create({
|
|
218
|
+
name: 'The Spree Handbook',
|
|
219
|
+
status: 'active',
|
|
220
|
+
prices: [{ currency: 'USD', amount: '19.99' }],
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Don't pass both.
|
|
225
|
+
|
|
205
226
|
Update, clone, or archive a product (deleting soft-deletes it):
|
|
206
227
|
|
|
207
228
|
|
|
@@ -397,29 +418,54 @@ curl 'https://api.mystore.com/api/v3/store/products/filters?category_id=ctg_xxx'
|
|
|
397
418
|
|
|
398
419
|
## Variants
|
|
399
420
|
|
|
400
|
-
|
|
421
|
+
**A product is not the thing you buy — a variant is.** The product is the listing; the variant is the actual item with a SKU, a price and stock.
|
|
422
|
+
|
|
423
|
+
That distinction is worth holding onto, because everything purchasable lives on the variant:
|
|
401
424
|
|
|
402
425
|
| Attribute | Description |
|
|
403
|
-
|
|
404
|
-
| `sku` |
|
|
405
|
-
| `barcode` | Barcode
|
|
426
|
+
|---|---|
|
|
427
|
+
| `sku` | Stock keeping unit |
|
|
428
|
+
| `barcode` | Barcode — UPC, EAN and so on |
|
|
406
429
|
| `price` | Price in the current currency |
|
|
407
|
-
| `original_price` | Compare-at price for showing
|
|
408
|
-
| `weight`, `height`, `width`, `depth` |
|
|
409
|
-
| `in_stock` | Whether
|
|
410
|
-
| `backorderable` | Whether
|
|
411
|
-
| `
|
|
430
|
+
| `original_price` | Compare-at price, for showing a reduction |
|
|
431
|
+
| `weight`, `height`, `width`, `depth` | Used for delivery rates and labels |
|
|
432
|
+
| `in_stock` / `purchasable` | Whether it can be bought right now |
|
|
433
|
+
| `backorderable` | Whether it can be ordered while out of stock |
|
|
434
|
+
| `preorder` / `preorder_ships_at` | Whether it's sold ahead of availability |
|
|
435
|
+
| `option_values` | What distinguishes it — Size: Small, Colour: Red |
|
|
436
|
+
| `options_text` | Those values as one readable string |
|
|
437
|
+
| `track_inventory` | Whether stock is counted at all |
|
|
412
438
|
|
|
413
|
-
|
|
439
|
+
Variants also carry the customs attributes — `hs_code`, `country_of_origin`, `customs_description` — used when a parcel crosses a border. See [Fees](fees.md#customs-classification).
|
|
414
440
|
|
|
415
|
-
Every product has
|
|
441
|
+
### Every product has at least one variant
|
|
416
442
|
|
|
417
|
-
|
|
443
|
+
There's no such thing as a product without one. A book with no size or colour still has a single variant holding its SKU, price and stock — you simply never show a picker for it.
|
|
418
444
|
|
|
419
|
-
|
|
445
|
+
> **NOTE:** Earlier versions of Spree had a special "master variant" alongside the real ones, which meant every query had to remember to exclude it. **That concept is gone.** A product's variants are all real, all purchasable, and all the same kind of thing.
|
|
420
446
|
|
|
421
|
-
|
|
422
|
-
|
|
447
|
+
### The default variant
|
|
448
|
+
|
|
449
|
+
`default_variant_id` names the variant that represents the product — the price shown on a listing page, and what "add to cart" means before anyone picks anything.
|
|
450
|
+
|
|
451
|
+
```typescript Store SDK
|
|
452
|
+
const product = await client.products.get('spree-tote')
|
|
453
|
+
|
|
454
|
+
product.default_variant_id // "var_xxx"
|
|
455
|
+
product.price // that variant's price
|
|
456
|
+
product.variant_count // 6
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
For a single-variant product that's the only variant. For a product with options it's the first one, unless you say otherwise. If the default is ever removed, another is promoted automatically — a product is never left without one.
|
|
460
|
+
|
|
461
|
+
Products can also carry `buy_box_variant_id`, naming the variant a marketplace has chosen to feature when several sellers offer the same listing.
|
|
462
|
+
|
|
463
|
+
### Options make variants
|
|
464
|
+
|
|
465
|
+
A product with option types has one variant per combination. A T-shirt in three sizes and two colours is six variants:
|
|
466
|
+
|
|
467
|
+
| SKU | Size | Colour |
|
|
468
|
+
|---|---|---|
|
|
423
469
|
| `TEE-S-R` | Small | Red |
|
|
424
470
|
| `TEE-S-G` | Small | Green |
|
|
425
471
|
| `TEE-M-R` | Medium | Red |
|
|
@@ -427,20 +473,18 @@ When a product has option types, each unique combination of option values create
|
|
|
427
473
|
| `TEE-L-R` | Large | Red |
|
|
428
474
|
| `TEE-L-G` | Large | Green |
|
|
429
475
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
Add a variant to an existing product via the Admin API (SKU, prices, and stock all live on the variant):
|
|
476
|
+
Adding one to an existing product:
|
|
433
477
|
|
|
434
478
|
|
|
435
479
|
```typescript Admin SDK
|
|
436
|
-
|
|
480
|
+
await adminClient.products.variants.create('prod_xxx', {
|
|
437
481
|
sku: 'TEE-L-R',
|
|
438
482
|
options: [
|
|
439
483
|
{ name: 'size', value: 'Large' },
|
|
440
484
|
{ name: 'color', value: 'Red' },
|
|
441
485
|
],
|
|
442
486
|
prices: [{ currency: 'USD', amount: '24.99' }],
|
|
443
|
-
|
|
487
|
+
stock_levels: [{ stock_location_id: 'sloc_xxx', count_on_hand: 30 }],
|
|
444
488
|
})
|
|
445
489
|
```
|
|
446
490
|
|
|
@@ -453,6 +497,8 @@ spree api post /products/prod_xxx/variants -d '{
|
|
|
453
497
|
```
|
|
454
498
|
|
|
455
499
|
|
|
500
|
+
Options are named by value rather than by ID, so you don't have to look up an option value before creating a variant that uses it.
|
|
501
|
+
|
|
456
502
|
## Option Types and Option Values
|
|
457
503
|
|
|
458
504
|
Option types define the axes of variation for a product (e.g., Size, Color, Material). Option values are the specific choices within each type (e.g., Small, Medium, Large).
|
|
@@ -517,9 +563,76 @@ spree api post /option_types -d '{
|
|
|
517
563
|
```
|
|
518
564
|
|
|
519
565
|
|
|
566
|
+
## Product Types
|
|
567
|
+
|
|
568
|
+
Merchants who sell more than one kind of thing end up repeating themselves. Every pair of shoes needs Size and Colour, belongs under Footwear, and wants a Material field. Every book needs an ISBN and an author.
|
|
569
|
+
|
|
570
|
+
A **product type** captures that once. Creating a product from a type gives it the right option types, the right categories, the right delivery profile, and a form asking for the fields that kind of product actually needs.
|
|
571
|
+
|
|
572
|
+
```mermaid
|
|
573
|
+
erDiagram
|
|
574
|
+
ProductType ||--o{ Product : "creates"
|
|
575
|
+
ProductType }o--o{ OptionType : "seeds"
|
|
576
|
+
ProductType }o--o{ Category : "seeds"
|
|
577
|
+
ProductType }o--o{ CustomFieldDefinition : "asks for"
|
|
578
|
+
ProductType }o--o| DeliveryProfile : "ships by"
|
|
579
|
+
|
|
580
|
+
ProductType {
|
|
581
|
+
string name
|
|
582
|
+
integer products_count
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```typescript Admin SDK
|
|
587
|
+
const shoes = await adminClient.productTypes.create({
|
|
588
|
+
name: 'Footwear',
|
|
589
|
+
option_type_ids: ['optt_size', 'optt_color'],
|
|
590
|
+
category_ids: ['ctg_footwear'],
|
|
591
|
+
delivery_profile_id: 'dp_standard',
|
|
592
|
+
custom_field_definitions: [
|
|
593
|
+
{ id: 'cfdef_material', required: true, sort_order: 0 },
|
|
594
|
+
],
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
await adminClient.products.create({
|
|
598
|
+
name: 'Trail Runner',
|
|
599
|
+
product_type_id: shoes.id,
|
|
600
|
+
status: 'draft',
|
|
601
|
+
})
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### A type is a template, not a controller
|
|
605
|
+
|
|
606
|
+
This is the part that determines how you should think about them.
|
|
607
|
+
|
|
608
|
+
> **WARNING:** **Editing a product type never rewrites existing products.** Add an option type to Footwear next month and the shoes you created last month are untouched.
|
|
609
|
+
|
|
610
|
+
That's deliberate. A merchant who adds a field to a type is describing what *new* products should look like — not asking Spree to silently restructure a live catalogue, invalidate URLs, or change what customers can pick.
|
|
611
|
+
|
|
612
|
+
So the pieces behave in two distinct ways:
|
|
613
|
+
|
|
614
|
+
| Part of the type | Behaviour |
|
|
615
|
+
|---|---|
|
|
616
|
+
| Option types, categories, delivery profile | **Stamped at creation.** Copied onto the product, then independent |
|
|
617
|
+
| Custom field definitions | **Live by reference.** The form always reflects the type as it is now |
|
|
618
|
+
|
|
619
|
+
Seeding is also **additive** — it adds what's missing and never removes what a product already has. Reassigning a product's type is therefore safe: it seeds the new type's option types and categories alongside whatever was already there.
|
|
620
|
+
|
|
621
|
+
If you *do* want an edited type to reach the products already using it, that's an explicit action:
|
|
622
|
+
|
|
623
|
+
```typescript Admin SDK
|
|
624
|
+
const { products_count } = await adminClient.productTypes.applyToProducts('pt_xxx')
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
It runs in the background and is additive like the rest — never a side effect of saving a type.
|
|
628
|
+
|
|
629
|
+
> **INFO:** `required` on a type's custom field is **advisory** — it marks the field in the dashboard but isn't enforced on write, since Spree saves the product and its fields in two steps. Validate in your own tooling if you need it enforced.
|
|
630
|
+
|
|
631
|
+
A type in use can't be deleted; its products would lose the structure they were built from.
|
|
632
|
+
|
|
520
633
|
## Media
|
|
521
634
|
|
|
522
|
-
Media can be attached to
|
|
635
|
+
Media can be attached to a product or to individual variants. When displaying a product, show the images for the selected variant, falling back to the product's own.
|
|
523
636
|
|
|
524
637
|
### Thumbnails
|
|
525
638
|
|
|
@@ -560,7 +673,7 @@ const product = await client.products.get('spree-tote', {
|
|
|
560
673
|
expand: ['media', 'variants'],
|
|
561
674
|
})
|
|
562
675
|
|
|
563
|
-
//
|
|
676
|
+
// The product's own images
|
|
564
677
|
product.media // [{ original_url: "https://cdn.../tote-front.jpg", position: 1 }, ...]
|
|
565
678
|
|
|
566
679
|
// Each variant has its own thumbnail and media_count
|