@spree/docs 0.1.182 → 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/addresses.md +106 -198
- package/dist/developer/core-concepts/architecture.md +97 -126
- package/dist/developer/core-concepts/calculators.md +75 -252
- package/dist/developer/core-concepts/carts.md +1 -1
- package/dist/developer/core-concepts/catalogs.md +140 -0
- package/dist/developer/core-concepts/channels.md +0 -4
- package/dist/developer/core-concepts/commissions.md +253 -0
- package/dist/developer/core-concepts/companies.md +240 -0
- package/dist/developer/core-concepts/customers.md +0 -3
- package/dist/developer/core-concepts/discounts.md +133 -0
- package/dist/developer/core-concepts/events.md +83 -576
- package/dist/developer/core-concepts/fees.md +144 -0
- package/dist/developer/core-concepts/imports-exports.md +105 -679
- package/dist/developer/core-concepts/inventory.md +114 -248
- package/dist/developer/core-concepts/markets.md +9 -12
- package/dist/developer/core-concepts/media.md +127 -16
- package/dist/developer/core-concepts/metafields.md +123 -200
- package/dist/developer/core-concepts/order-totals.md +110 -0
- package/dist/developer/core-concepts/orders.md +1 -1
- package/dist/developer/core-concepts/payments.md +11 -14
- package/dist/developer/core-concepts/pricing.md +11 -13
- package/dist/developer/core-concepts/products.md +191 -62
- package/dist/developer/core-concepts/promotions.md +12 -11
- package/dist/developer/core-concepts/search-filtering.md +2 -4
- package/dist/developer/core-concepts/sellers.md +210 -0
- package/dist/developer/core-concepts/staff-roles.md +56 -23
- package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
- package/dist/developer/core-concepts/taxes.md +125 -113
- package/dist/developer/core-concepts/translations.md +61 -68
- package/dist/developer/core-concepts/webhooks.md +25 -59
- package/dist/developer/how-to/custom-promotion.md +3 -3
- package/package.json +1 -1
- package/dist/developer/core-concepts/companies-and-catalogs.md +0 -81
- package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
|
@@ -1,16 +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
|
-
import { Since } from '/snippets/since.mdx';
|
|
8
|
-
|
|
9
7
|
## Overview
|
|
10
8
|
|
|
11
|
-
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
|
+
|
|
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
|
|
12
59
|
|
|
13
|
-
|
|
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
|
|
14
86
|
|
|
15
87
|
A media record carries:
|
|
16
88
|
|
|
@@ -21,9 +93,9 @@ A media record carries:
|
|
|
21
93
|
- **Preprocessed named variants** for fast delivery
|
|
22
94
|
- **`variant_ids`** — which product variants the media represents. An empty array means it represents the product as a whole.
|
|
23
95
|
|
|
24
|
-
### Product-level
|
|
96
|
+
### Product-level gallery
|
|
25
97
|
|
|
26
|
-
|
|
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.
|
|
27
99
|
|
|
28
100
|
#### Uploading a product-level image
|
|
29
101
|
|
|
@@ -106,7 +178,7 @@ The Store API's `media` field on a product returns its gallery — product-level
|
|
|
106
178
|
|
|
107
179
|
This dual rendering means existing storefronts keep working during the upgrade; new uploads attach to the product, and you opt into a [one-shot migration](/v5/developer/upgrades/5.4-to-5.5) to re-home legacy variant-pinned data when convenient.
|
|
108
180
|
|
|
109
|
-
### Video
|
|
181
|
+
### Video
|
|
110
182
|
|
|
111
183
|
A product gallery can hold video as well as images. Spree supports both ways merchants usually have it:
|
|
112
184
|
|
|
@@ -186,7 +258,7 @@ Where the poster comes from, in order:
|
|
|
186
258
|
|
|
187
259
|
Spree does not extract a frame from an uploaded video, so give hosted video and Vimeo links a poster if you want them to show a still.
|
|
188
260
|
|
|
189
|
-
### Focal Point
|
|
261
|
+
### Focal Point
|
|
190
262
|
|
|
191
263
|
`focal_point_x` and `focal_point_y` mark the part of an image that must stay in frame when a storefront crops it to a different shape. Both are fractions between 0 and 1, measured from the top left, so `{ x: 0.5, y: 0.5 }` is dead centre — which is also what a storefront should assume when they are null.
|
|
192
264
|
|
|
@@ -199,7 +271,7 @@ await client.products.media.update('prod_86Rf07xd4z', 'media_k5nR8xLq', {
|
|
|
199
271
|
|
|
200
272
|
Spree stores the focal point and serves it; the cropping itself is the storefront's decision, since only it knows the shape it needs.
|
|
201
273
|
|
|
202
|
-
### Named Variant Sizes
|
|
274
|
+
### Named Variant Sizes
|
|
203
275
|
|
|
204
276
|
When an image is uploaded, Spree automatically generates optimized versions in the background:
|
|
205
277
|
|
|
@@ -319,15 +391,54 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=media,vari
|
|
|
319
391
|
| `focal_point_x` | number \| null | Horizontal focal point (0.0–1.0) |
|
|
320
392
|
| `focal_point_y` | number \| null | Vertical focal point (0.0–1.0) |
|
|
321
393
|
| `external_video_url` | string \| null | External video URL (YouTube/Vimeo) |
|
|
322
|
-
| `video_provider` | string \| null | `youtube` or `vimeo`, derived from the link
|
|
323
|
-
| `video_embed_url` | string \| null | Embeddable player URL, derived from the link
|
|
324
|
-
| `video_url` | string \| null | Uploaded video file URL
|
|
325
|
-
| `poster_url` | string \| null | Still frame for a video
|
|
394
|
+
| `video_provider` | string \| null | `youtube` or `vimeo`, derived from the link |
|
|
395
|
+
| `video_embed_url` | string \| null | Embeddable player URL, derived from the link |
|
|
396
|
+
| `video_url` | string \| null | Uploaded video file URL |
|
|
397
|
+
| `poster_url` | string \| null | Still frame for a video |
|
|
326
398
|
| `original_url` | string \| null | Full-size image URL (inline disposition) |
|
|
327
399
|
| `mini_url` ... `xlarge_url` | string \| null | Named variant URLs |
|
|
328
|
-
| `download_url` | string \| null | Same blob as `original_url` but with `Content-Disposition: attachment`. Admin API only. |
|
|
329
400
|
|
|
330
|
-
|
|
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
|
|
331
442
|
|
|
332
443
|
Spree uses [libvips](https://www.libvips.org/) for image processing. Images are automatically:
|
|
333
444
|
|
|
@@ -1,148 +1,125 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
description: Add
|
|
2
|
+
title: Custom Fields
|
|
3
|
+
description: Add your own structured, typed data to products, orders and other records — searchable, filterable, and editable in the dashboard.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
import { Since } from '/snippets/since.mdx';
|
|
7
|
-
|
|
8
|
-
|
|
9
6
|
## Overview
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
Sooner or later you need to store something Spree doesn't have a field for. A fabric composition. A care instruction. A gift message. An ID from the system you sync with.
|
|
9
|
+
|
|
10
|
+
Custom fields are how you do that without changing the database. You declare a field once — its name, its type, who can see it — and from then on it can be set on any record of that kind, edited in the dashboard, returned by the API, and searched or filtered like any built-in field.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
> **NOTE:** Custom fields were called **metafields** before Spree 6.0. The API and SDK have always called them custom fields; now the rest of Spree does too.
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
- Custom business logic fields
|
|
17
|
-
- Integration data from external systems
|
|
18
|
-
- Order-specific custom attributes
|
|
14
|
+
## Definitions and values
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
There are two halves, and it helps to keep them straight:
|
|
21
17
|
|
|
22
18
|
```mermaid
|
|
23
19
|
erDiagram
|
|
24
|
-
|
|
25
|
-
Product ||--o{
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
CustomFieldDefinition ||--o{ CustomField : "gives shape to"
|
|
21
|
+
Product ||--o{ CustomField : "has values"
|
|
22
|
+
Order ||--o{ CustomField : "has values"
|
|
23
|
+
Customer ||--o{ CustomField : "has values"
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
CustomFieldDefinition {
|
|
30
26
|
string namespace
|
|
31
27
|
string key
|
|
32
|
-
string
|
|
33
|
-
string
|
|
28
|
+
string label
|
|
29
|
+
string field_type
|
|
34
30
|
string resource_type
|
|
35
|
-
|
|
31
|
+
boolean storefront_visible
|
|
36
32
|
boolean searchable
|
|
37
33
|
boolean sortable
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
string
|
|
42
|
-
|
|
43
|
-
string
|
|
36
|
+
CustomField {
|
|
37
|
+
string key
|
|
38
|
+
string label
|
|
39
|
+
string field_type
|
|
40
|
+
any value
|
|
44
41
|
}
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
subgraph Definition
|
|
50
|
-
A[MetafieldDefinition] --> B["namespace: properties"]
|
|
51
|
-
A --> C["key: manufacturer"]
|
|
52
|
-
A --> D["type: ShortText"]
|
|
53
|
-
A --> E["resource_type: Product"]
|
|
54
|
-
A --> F["display_on: both"]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
subgraph Instance
|
|
58
|
-
G[Metafield] --> H["value: Wilson"]
|
|
59
|
-
G --> I["resource: Product #1"]
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
A -.->|"defines schema"| G
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
- **MetafieldDefinition** — the blueprint that defines the data type, target resource, and visibility
|
|
66
|
-
- **Metafield** — stores the actual value for a specific resource instance
|
|
67
|
-
|
|
68
|
-
## Data Types
|
|
44
|
+
- A **definition** is the declaration: "products have a Material, it is short text, shoppers may see it." You create it once.
|
|
45
|
+
- A **custom field** is one record's answer: this product's material is `100% Cotton`.
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|------|-------------|----------------|
|
|
72
|
-
| Short Text | Brief text fields | SKU codes, brand names, tags |
|
|
73
|
-
| Long Text | Longer text content | Care instructions, notes |
|
|
74
|
-
| Rich Text | Formatted HTML content | Product descriptions with formatting |
|
|
75
|
-
| Number | Numeric values | Weight, quantity, ratings |
|
|
76
|
-
| Boolean | True/false flags | Is featured, requires signature |
|
|
77
|
-
| JSON | Structured data | Configuration, complex objects |
|
|
47
|
+
Because the definition carries the type and the label, the dashboard can build an editing form for it automatically, and your storefront gets a value it can trust the shape of.
|
|
78
48
|
|
|
79
|
-
##
|
|
49
|
+
## Field types
|
|
80
50
|
|
|
81
|
-
|
|
51
|
+
| Type | For | Example |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `short_text` | A word or a line | `Wilson`, `100% Cotton` |
|
|
54
|
+
| `long_text` | A paragraph | Care instructions |
|
|
55
|
+
| `rich_text` | Formatted HTML | A spec sheet with lists and bold |
|
|
56
|
+
| `number` | A quantity or measure | `3.5`, `120` |
|
|
57
|
+
| `boolean` | A yes or no | Requires a signature |
|
|
58
|
+
| `json` | Structured data | A sizing chart |
|
|
82
59
|
|
|
83
|
-
|
|
84
|
-
|------------|:---------:|:---------:|----------|
|
|
85
|
-
| `both` | Yes | Yes | Public product specifications |
|
|
86
|
-
| `back_end` | No | Yes | Internal notes, integration IDs |
|
|
60
|
+
## Who can see it
|
|
87
61
|
|
|
88
|
-
|
|
62
|
+
`storefront_visible` decides whether a field ever leaves the back office:
|
|
89
63
|
|
|
90
|
-
|
|
64
|
+
| `storefront_visible` | Store API | Admin API | Use it for |
|
|
65
|
+
|---|:---:|:---:|---|
|
|
66
|
+
| `true` | Yes | Yes | Anything a shopper should see — materials, specs |
|
|
67
|
+
| `false` | No | Yes | Internal notes, supplier codes, sync IDs |
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|------|--------|---------------------|
|
|
94
|
-
| `searchable` | Values are included in product text search | `short_text`, `long_text`, `number` |
|
|
95
|
-
| `sortable` | Listings can sort with `sort=cf_{namespace}_{key}` (prefix `-` for descending), e.g. `cf_custom_label` | `short_text`, `number` |
|
|
69
|
+
> **WARNING:** `storefront_visible: false` genuinely withholds the field from the Store API — it is not hidden in the response, it is absent. Treat it as the boundary between what a customer may read and what only staff may.
|
|
96
70
|
|
|
97
|
-
|
|
71
|
+
## Declaring a field
|
|
98
72
|
|
|
99
|
-
### Filtering
|
|
100
73
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
74
|
+
```typescript Admin SDK
|
|
75
|
+
const definition = await adminClient.customFieldDefinitions.create({
|
|
76
|
+
resource_type: 'Spree::Product',
|
|
77
|
+
namespace: 'properties',
|
|
78
|
+
key: 'material',
|
|
79
|
+
label: 'Material',
|
|
80
|
+
field_type: 'short_text',
|
|
81
|
+
storefront_visible: true,
|
|
82
|
+
searchable: true,
|
|
83
|
+
})
|
|
107
84
|
```
|
|
108
85
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
In the admin dashboard, these fields appear in the products table's column selector, Sort dropdown, and filter panel — hidden by default, so admins opt in per column.
|
|
121
|
-
|
|
122
|
-
## Supported Resources
|
|
86
|
+
```bash CLI
|
|
87
|
+
spree api post /custom_field_definitions -d '{
|
|
88
|
+
"resource_type": "Spree::Product",
|
|
89
|
+
"namespace": "properties",
|
|
90
|
+
"key": "material",
|
|
91
|
+
"label": "Material",
|
|
92
|
+
"field_type": "short_text",
|
|
93
|
+
"storefront_visible": true,
|
|
94
|
+
"searchable": true
|
|
95
|
+
}'
|
|
96
|
+
```
|
|
123
97
|
|
|
124
|
-
Metafields can be attached to most Spree resources including Products, Variants, Orders, Line Items, Taxons, Payments, Shipments, Gift Cards, Store Credits, and more.
|
|
125
98
|
|
|
126
|
-
|
|
99
|
+
`namespace` keeps groups of fields apart, so an integration's `product_id` never collides with yours. Together they form the key you'll see in responses: `properties.material`. Both are normalized to snake_case.
|
|
127
100
|
|
|
128
|
-
|
|
101
|
+
Definitions can also be managed in the dashboard under **Settings → Custom Fields**, which is where merchants usually add them.
|
|
129
102
|
|
|
130
|
-
|
|
103
|
+
## Setting and reading values
|
|
131
104
|
|
|
132
|
-
| Namespace | Example Keys | Purpose |
|
|
133
|
-
|-----------|-------------|---------|
|
|
134
|
-
| `properties` | `manufacturer`, `material`, `fit` | Product specifications |
|
|
135
|
-
| `shopify` | `product_id`, `variant_id` | Integration data |
|
|
136
|
-
| `flags` | `featured`, `requires_approval` | Feature flags |
|
|
137
|
-
| `custom` | `gift_message`, `delivery_notes` | Business-specific fields |
|
|
138
105
|
|
|
139
|
-
|
|
106
|
+
```typescript Admin SDK
|
|
107
|
+
// Set a value on a product
|
|
108
|
+
await adminClient.products.customFields.create('prod_xxx', {
|
|
109
|
+
custom_field_definition_id: definition.id,
|
|
110
|
+
value: '100% Cotton',
|
|
111
|
+
})
|
|
112
|
+
```
|
|
140
113
|
|
|
141
|
-
|
|
114
|
+
```bash CLI
|
|
115
|
+
spree api post /products/prod_xxx/custom_fields -d '{
|
|
116
|
+
"custom_field_definition_id": "cfdef_xxx",
|
|
117
|
+
"value": "100% Cotton"
|
|
118
|
+
}'
|
|
119
|
+
```
|
|
142
120
|
|
|
143
|
-
Metafields with `display_on` set to `both` are included in Store API responses when you [request the `custom_fields` expand](../../api-reference/store-api/relations.md):
|
|
144
121
|
|
|
145
|
-
|
|
122
|
+
Reading them from a storefront is an expand on whatever you already fetch:
|
|
146
123
|
|
|
147
124
|
|
|
148
125
|
```typescript Store SDK
|
|
@@ -150,44 +127,29 @@ const product = await client.products.get('spree-tote', {
|
|
|
150
127
|
expand: ['custom_fields'],
|
|
151
128
|
})
|
|
152
129
|
|
|
153
|
-
product.custom_fields?.forEach(field => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
})
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
```typescript Admin SDK
|
|
162
|
-
const product = await adminClient.products.get('prod_86Rf07xd4z', {
|
|
163
|
-
expand: ['custom_fields'],
|
|
130
|
+
product.custom_fields?.forEach((field) => {
|
|
131
|
+
field.key // "properties.material"
|
|
132
|
+
field.label // "Material"
|
|
133
|
+
field.value // "100% Cotton"
|
|
134
|
+
field.field_type // "short_text"
|
|
164
135
|
})
|
|
165
136
|
```
|
|
166
137
|
|
|
167
138
|
```bash cURL
|
|
168
139
|
curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=custom_fields' \
|
|
169
|
-
-H 'X-Spree-
|
|
140
|
+
-H 'X-Spree-API-Key: pk_xxx'
|
|
170
141
|
```
|
|
171
142
|
|
|
172
143
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
```json
|
|
144
|
+
```json Response
|
|
176
145
|
{
|
|
177
146
|
"id": "prod_86Rf07xd4z",
|
|
178
|
-
"name": "Spree
|
|
147
|
+
"name": "Spree Tote",
|
|
179
148
|
"custom_fields": [
|
|
180
149
|
{
|
|
181
150
|
"id": "cf_k5nR8xLq",
|
|
182
|
-
"label": "Manufacturer",
|
|
183
|
-
"key": "properties.manufacturer",
|
|
184
|
-
"field_type": "short_text",
|
|
185
|
-
"value": "Wilson"
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
"id": "cf_m3Rp9wXz",
|
|
189
|
-
"label": "Material",
|
|
190
151
|
"key": "properties.material",
|
|
152
|
+
"label": "Material",
|
|
191
153
|
"field_type": "short_text",
|
|
192
154
|
"value": "100% Cotton"
|
|
193
155
|
}
|
|
@@ -195,96 +157,57 @@ curl 'https://api.mystore.com/api/v3/store/products/spree-tote?expand=custom_fie
|
|
|
195
157
|
}
|
|
196
158
|
```
|
|
197
159
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
## Admin Management
|
|
201
|
-
|
|
202
|
-
### Managing Definitions
|
|
160
|
+
Each value arrives with its own label and type, so you can render a spec table straight from the array without hardcoding which fields exist.
|
|
203
161
|
|
|
204
|
-
|
|
162
|
+
## Searching, sorting and filtering
|
|
205
163
|
|
|
206
|
-
|
|
164
|
+
A custom field can take part in product listings — but only if you ask, because indexing everything by default would be wasteful.
|
|
207
165
|
|
|
166
|
+
| Flag | What it enables | Works with |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `searchable` | The value is matched by product text search | `short_text`, `long_text`, `number` |
|
|
169
|
+
| `sortable` | Listings can be ordered by it | `short_text`, `number` |
|
|
208
170
|
|
|
209
|
-
|
|
210
|
-
import { createAdminClient } from '@spree/admin-sdk'
|
|
211
|
-
|
|
212
|
-
const client = createAdminClient({
|
|
213
|
-
baseUrl: 'https://store.example.com',
|
|
214
|
-
secretKey: 'sk_xxx',
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
const definition = await client.customFieldDefinitions.create({
|
|
218
|
-
resource_type: 'Spree::Product',
|
|
219
|
-
namespace: 'properties',
|
|
220
|
-
key: 'manufacturer',
|
|
221
|
-
label: 'Manufacturer',
|
|
222
|
-
field_type: 'short_text',
|
|
223
|
-
storefront_visible: true,
|
|
224
|
-
})
|
|
171
|
+
Setting either flag also makes the field **filterable**. Filter and sort with the field's `cf_` key, on both APIs:
|
|
225
172
|
|
|
226
|
-
await client.customFieldDefinitions.update(definition.id, { storefront_visible: false })
|
|
227
|
-
await client.customFieldDefinitions.delete(definition.id)
|
|
228
173
|
```
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
"resource_type": "Spree::Product",
|
|
233
|
-
"namespace": "properties",
|
|
234
|
-
"key": "manufacturer",
|
|
235
|
-
"label": "Manufacturer",
|
|
236
|
-
"field_type": "short_text",
|
|
237
|
-
"storefront_visible": true
|
|
238
|
-
}'
|
|
174
|
+
GET /api/v3/store/products?q[cf_properties_material_i_cont]=wool
|
|
175
|
+
GET /api/v3/store/products?sort=-cf_properties_weight
|
|
176
|
+
GET /api/v3/admin/products?q[cf_properties_weight_gteq]=3.5
|
|
239
177
|
```
|
|
240
178
|
|
|
179
|
+
| Field type | Comparisons available |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `short_text`, `long_text` | `i_cont`, `cont`, `eq`, `not_eq`, `start`, `end`, `present`, `blank` |
|
|
182
|
+
| `number` | `eq`, `gt`, `gteq`, `lt`, `lteq`, `present`, `blank` |
|
|
241
183
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
When editing a resource (e.g., a product), metafields appear in a dedicated section. The admin panel automatically builds forms for all defined metafields.
|
|
245
|
-
|
|
246
|
-
To set a value programmatically, use the resource's nested `customFields` accessor (parent ID first):
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
```typescript Admin SDK
|
|
250
|
-
await client.products.customFields.create('prod_xxx', {
|
|
251
|
-
custom_field_definition_id: 'cfdef_xxx',
|
|
252
|
-
value: 'Wilson',
|
|
253
|
-
})
|
|
254
|
-
```
|
|
184
|
+
Use `i_cont` for case-insensitive matching. A comparison that doesn't suit the field's type is ignored rather than rejected, so a stale filter in a saved view can't break a page.
|
|
255
185
|
|
|
256
|
-
|
|
257
|
-
spree api post /products/prod_xxx/custom_fields -d '{
|
|
258
|
-
"custom_field_definition_id": "cfdef_xxx",
|
|
259
|
-
"value": "Wilson"
|
|
260
|
-
}'
|
|
261
|
-
```
|
|
186
|
+
> **INFO:** If you use Meilisearch, re-index after changing these flags so the new fields are picked up. Substring matching (`i_cont`, `cont`, `start`, `end`) is a database-provider feature; Meilisearch handles equality, ranges and presence. See [Search & Filtering](search-filtering.md).
|
|
262
187
|
|
|
188
|
+
In the dashboard, fields with these flags become available in the product table's column picker, sort menu and filter panel — off by default, so merchants opt in per column.
|
|
263
189
|
|
|
264
|
-
##
|
|
190
|
+
## What can carry custom fields
|
|
265
191
|
|
|
266
|
-
|
|
192
|
+
Most things you'd want to annotate: products, variants, orders, line items, customers, categories, payments, fulfillments, gift cards and store credits, among others.
|
|
267
193
|
|
|
268
|
-
|
|
269
|
-
|---------|-----------|----------|
|
|
270
|
-
| **Purpose** | Merchant-defined structured attributes | Developer escape hatch — integration IDs, sync state |
|
|
271
|
-
| **Schema** | Defined via MetafieldDefinitions | Schemaless JSON — no definition required |
|
|
272
|
-
| **Validation** | Type-specific (text, number, boolean, etc.) | None — accepts any JSON-serializable data |
|
|
273
|
-
| **Visibility** | Configurable (admin-only or public) | Write-only in Store API, readable in Admin API |
|
|
274
|
-
| **Admin UI** | Dedicated management forms | JSON preview |
|
|
275
|
-
| **Data Types** | 6 specific types | Any JSON value |
|
|
276
|
-
| **Organization** | Namespaced (`namespace.key`) | Flat key-value structure |
|
|
277
|
-
| **Queryable** | Via SQL joins, Ransack scopes, search providers | Via JSONB operators (PostgreSQL) |
|
|
194
|
+
## Custom fields or metadata?
|
|
278
195
|
|
|
279
|
-
|
|
196
|
+
Spree has two ways to store your own data, and they are not competing — they solve different problems.
|
|
280
197
|
|
|
281
|
-
|
|
198
|
+
| | Custom fields | [Metadata](../customization/metadata.md) |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| **For** | Data a merchant curates | Data your code keeps |
|
|
201
|
+
| **Shape** | Declared and typed | Any JSON, no declaration |
|
|
202
|
+
| **Dashboard** | Proper edit forms | Raw JSON |
|
|
203
|
+
| **Visibility** | Configurable per field | Never read by the Store API |
|
|
204
|
+
| **Searchable** | Yes, opt in | Not through the product search |
|
|
282
205
|
|
|
283
|
-
|
|
206
|
+
Put it simply: **custom fields are for people, metadata is for machines.** If a merchant should type it, define a custom field. If it is a sync token or an external ID that only your integration reads, use metadata.
|
|
284
207
|
|
|
285
|
-
## Related
|
|
208
|
+
## Related
|
|
286
209
|
|
|
287
|
-
- [Metadata](../customization/metadata.md) —
|
|
288
|
-
- [Products](products.md) —
|
|
289
|
-
- [
|
|
290
|
-
- [Admin SDK](../sdk/admin/resources.md) —
|
|
210
|
+
- [Metadata](../customization/metadata.md) — the machine-readable alternative
|
|
211
|
+
- [Products](products.md) — the most common place for custom fields
|
|
212
|
+
- [Search & Filtering](search-filtering.md) — how filters and search work
|
|
213
|
+
- [Admin SDK](../sdk/admin/resources.md) — managing definitions and values in TypeScript
|