@doswiftly/storefront-operations 13.1.0 → 14.0.0

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/AGENTS.md CHANGED
@@ -27,7 +27,7 @@ consumer's `codegen.ts` references this package's `.graphql` files as
27
27
  live in the consumer's repo.
28
28
 
29
29
  <!-- AUTOGEN:STATS:BEGIN — auto-regenerated, do not edit by hand -->
30
- - **Schema version**: 13.1.0
30
+ - **Schema version**: 14.0.0
31
31
  - **Queries**: 50
32
32
  - **Mutations**: 40
33
33
  - **Fragments**: 100
package/CHANGELOG.md CHANGED
@@ -1,5 +1,186 @@
1
1
  # Changelog
2
2
 
3
+ ## 14.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 1fda2ab: **BREAKING**: URL identifier unification (`slug` → `handle`) + type renames for naming consistency.
8
+
9
+ ### What changed
10
+
11
+ URL-friendly identifiers across the storefront GraphQL API now consistently use `handle` (previously a mix of `slug` and `handle`). Canonical product brand entity renamed from `BrandSummary` to `Brand`. Shop branding type `Brand` renamed to `ShopBrand` to free the `Brand` name for the product entity.
12
+
13
+ ### Field renames (GraphQL response)
14
+
15
+ | Type | Old | New |
16
+ | ---------------------------- | ------ | -------- |
17
+ | `Category` | `slug` | `handle` |
18
+ | `BlogPost` | `slug` | `handle` |
19
+ | `BlogCategory` | `slug` | `handle` |
20
+ | `BlogTag` | `slug` | `handle` |
21
+ | `LoyaltyReward` | `slug` | `handle` |
22
+ | `BrandFilter` (input) | `slug` | `handle` |
23
+ | `BrandFilterValue` | `slug` | `handle` |
24
+ | `ProductAttributeDefinition` | `slug` | `handle` |
25
+ | `CategoryFilterOption` | `slug` | `handle` |
26
+
27
+ ### Type renames
28
+ - `BrandSummary` → `Brand` (canonical product brand entity, e.g. Funko, Nike). Update `__typename` checks and fragment spreads.
29
+ - `Brand` (shop branding) → `ShopBrand` (tenant-scoped shop metadata: logo, slogan, colors).
30
+
31
+ ### Query args renamed
32
+
33
+ ```graphql
34
+ # Old
35
+ query { category(slug: "figurki") { id name } }
36
+ query { blogPost(slug: "post-handle") { title } }
37
+ query BlogPosts($categorySlug: String, $tagSlug: String) {
38
+ blogPosts(categorySlug: $categorySlug, tagSlug: $tagSlug) { ... }
39
+ }
40
+
41
+ # New
42
+ query { category(handle: "figurki") { id name } }
43
+ query { blogPost(handle: "post-handle") { title } }
44
+ query BlogPosts($categoryHandle: String, $tagHandle: String) {
45
+ blogPosts(categoryHandle: $categoryHandle, tagHandle: $tagHandle) { ... }
46
+ }
47
+ ```
48
+
49
+ ### Brand filter input
50
+
51
+ ```graphql
52
+ # Old
53
+ products(filters: [{ brand: { slug: "funko" } }]) { ... }
54
+
55
+ # New
56
+ products(filters: [{ brand: { handle: "funko" } }]) { ... }
57
+ ```
58
+
59
+ ### MenuItem resource union
60
+
61
+ ```graphql
62
+ # Old
63
+ fragment MenuItem on MenuItem {
64
+ resource {
65
+ __typename
66
+ ... on Category {
67
+ id
68
+ slug
69
+ name
70
+ }
71
+ ... on BrandSummary {
72
+ id
73
+ slug
74
+ name
75
+ logo
76
+ }
77
+ }
78
+ }
79
+
80
+ # New
81
+ fragment MenuItem on MenuItem {
82
+ resource {
83
+ __typename
84
+ ... on Category {
85
+ id
86
+ handle
87
+ name
88
+ }
89
+ ... on Brand {
90
+ id
91
+ handle
92
+ name
93
+ logo
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Migration steps
100
+ 1. Update to `@doswiftly/storefront-operations@^14.0.0` + `@doswiftly/storefront-sdk@^14.0.0`.
101
+ 2. Run your codegen — TypeScript will surface every call site that needs update.
102
+ 3. Find/replace in storefront code:
103
+ - `.slug` → `.handle` on response fields listed above.
104
+ - `__typename === 'BrandSummary'` → `__typename === 'Brand'`.
105
+ - `{ brand: { slug } }` filter input → `{ brand: { handle } }`.
106
+ - `categorySlug`/`tagSlug` query args → `categoryHandle`/`tagHandle`.
107
+ 4. Update GraphQL fragments selecting renamed fields.
108
+ 5. Re-run codegen, fix any remaining TypeScript errors.
109
+
110
+ ### Why
111
+
112
+ A single naming convention (`handle`) across all URL identifiers removes ambiguity and matches common e-commerce GraphQL convention. The `BrandSummary` suffix was misleading — no full `Brand` ObjectType existed, so the suffix suggested a missing parent type. Shop branding `Brand` blocked the cleaner name for the product entity.
113
+
114
+ Full migration guide with code examples: <https://docs.doswiftly.pl/storefront-developer/migration-14.0>
115
+
116
+ Naming conventions reference: <https://docs.doswiftly.pl/storefront-developer/api/naming-conventions>
117
+
118
+ ### Minor Changes
119
+
120
+ - 507d487: Storefront GraphQL: nowy `attributeOptionsSearch(input)` query — paginated + searchable lista opcji atrybutu (Relay Connection).
121
+
122
+ Use case: storefront UI z dużymi filtrami atrybutów (np. 800+ licencji, materiałów, rozmiarów, kolorów) — dropdown z autocomplete i paginacją bez zassania całej listy. `productFilters.attributes[].filterValues[]` zostaje jako quick browse facets (top N), `attributeOptionsSearch` służy do drill-into specific filter card z searchem.
123
+
124
+ Dla **marek/producentów** preferuj dedykowane API `productFilters.brands[]` (Brand jest pełnoprawną encją z logo i productCount) — `attributeOptionsSearch(handle: "marka")` jest legacy fallback dla katalogów bez kanonicznej Brand entity.
125
+
126
+ **Input** (`AttributeOptionsSearchInput`):
127
+ - `handle: String!` — slug atrybutu (np. `"marka"`)
128
+ - `contextInput: AvailableFiltersInput` — produktowy kontekst (kategoria/kolekcja/search/currentFilters) z exclude-self semantyką
129
+ - `first: Int` (1–100, default 50)
130
+ - `after: String` — opaque cursor
131
+ - `search: String` — case-insensitive ILIKE (max 100 znaków)
132
+ - `sort: AttributeOptionsSearchSort` — `COUNT_DESC` (default, most popular first) lub `LABEL_ASC` (alfabetycznie z polskim collation)
133
+
134
+ **Output** (`AttributeFilterValueConnection`): `edges { cursor node }`, `nodes` (shortcut), `pageInfo`, `totalCount`. Wspiera atrybuty `SELECT`/`CHECKBOX`/`RADIO`/`COLOR` (z bazy `attribute_options`) oraz `TEXT`/`TEXTAREA` (z aggregated unique values).
135
+
136
+ **Migration**: brak breaking changes — `productFilters` zachowuje obecny shape. Jeśli budujesz "Pokaż wszystkie marki" UI, przenoś logikę na nowy query żeby uniknąć over-fetching.
137
+
138
+ - 6f8d873: `CartWarningCode` enum gains a fourth value, `SHIPPING_METHOD_AUTO_CLEARED`.
139
+
140
+ Cart line mutations (`cartAddLines`, `cartUpdateLines`, `cartRemoveLines`) now reconcile the selected shipping method against cart contents on every call. When a mutation leaves the cart with no items that require physical delivery (digital-only or empty) but a shipping method is still set, the method is cleared automatically and the mutation response surfaces a `CartWarning { code: "SHIPPING_METHOD_AUTO_CLEARED", target: "selectedShippingMethod", message: "…" }`.
141
+
142
+ This closes the gap where a mixed cart that lost its last physical line via the storefront UI would persist a stale shipping method and fail later at checkout submit with `FORBIDDEN_SHIPPING_METHOD`. There is no escape hatch needed on the storefront — the mutation accepts non-null `shippingMethodId` only, so the backend takes responsibility for the transition.
143
+
144
+ **Storefront impact**: none required. Reads of `cart.selectedShippingMethod` already returned `null` once cleared. Apps that already consume the `warnings[]` array (Apps that follow the standard `userErrors[]` + `warnings[]` payload shape) will surface the auto-clear toast for free. Apps that ignore `warnings[]` continue to work — the mutation still succeeds and the cart is left in a consistent state.
145
+
146
+ **Locale**: `message` is translated server-side (`Accept-Language`) — for example, `pl` returns "Wybrana metoda wysyłki została usunięta, ponieważ koszyk nie wymaga już dostawy.", `en` returns "The selected shipping method was removed because the cart no longer requires delivery."
147
+
148
+ The warning fires only on the line mutation that triggers the transition, never on subsequent reads or unrelated mutations. Decrementing a physical line quantity from `3` to `1`, removing one physical line while another remains, or adding digital lines to a mixed cart all leave the shipping method untouched.
149
+
150
+ - 1fda2ab: Storefront navigation menus: typed `resource` field eksponowany w `MenuItem` fragment + nowy typ linku `BRAND`.
151
+
152
+ **Co nowego**:
153
+ - `MenuItem.resource: MenuItemResource` — typed union (`Category` / `Collection` / `ShopPage` / `Product` / `BrandSummary`) wystawiony w `MenuItem` fragment na wszystkich 3 poziomach zagnieżdżenia. Każdy member zwraca `slug` (Category/Product/BrandSummary) lub `handle` (Collection/ShopPage) gotowy do budowy własnego URL.
154
+ - `MenuItemType.BRAND` — nowy typ linku menu dla marek producentów. `BrandSummary` w `resource` union (`id`, `name`, `slug`, `logo`).
155
+ - `MenuItem.url` dla `BRAND` resolve'uje się serwerowo jako `/brands/<slug>` — spójnie z istniejącą konwencją `/categories|/collections|/pages|/products/<slug>`.
156
+
157
+ **Dwa sposoby renderingu link target**:
158
+ 1. **Standard route convention** — użyj `item.url` jak dotąd. Działa jeśli storefront ma routy `/categories/[slug]`, `/collections/[handle]`, `/pages/[handle]`, `/products/[handle]`, `/brands/[slug]`.
159
+ 2. **Custom routing** (Next.js single-segment, Remix, itd.) — odczytaj `item.resource.__typename` + per-type `slug`/`handle`:
160
+
161
+ ```ts
162
+ function buildHref(item: MenuItem): string {
163
+ if (!item.resource) return item.url ?? "#";
164
+ switch (item.resource.__typename) {
165
+ case "Category":
166
+ return `/${item.resource.slug}`;
167
+ case "Collection":
168
+ return `/${item.resource.handle}`;
169
+ case "ShopPage":
170
+ return `/${item.resource.handle}`;
171
+ case "Product":
172
+ return `/p/${item.resource.handle}`;
173
+ case "BrandSummary":
174
+ return `/marka/${item.resource.slug}`;
175
+ }
176
+ return item.url ?? "#";
177
+ }
178
+ ```
179
+
180
+ **Performance**: wszystkie resource lookupy są batchowane per request — zero N+1 nawet w głębokich menu.
181
+
182
+ **Migration**: brak breaking changes. Kod używający tylko `item.url` działa bez zmian. `resource` jest opt-in dopóki nie zaczniesz go selekcjonować w custom fragmencie.
183
+
3
184
  ## 13.1.0
4
185
 
5
186
  ### Minor Changes
package/README.md CHANGED
@@ -168,7 +168,7 @@ full executable body of each operation.
168
168
 
169
169
  | Operation | Description |
170
170
  | --- | --- |
171
- | `Product` | Fetches a single product by `id` or `handle` (URL slug). Pass either — whichever is provided wins; if both are missing, returns null. Returns null if the product is not storefront-accessible (must be `ACTIVE` status with `PUBLIC` or `BUNDLE_ONLY` visibility). |
171
+ | `Product` | Fetches a single product by `id` or `handle` (URL-friendly identifier). Pass either — whichever is provided wins; if both are missing, returns null. Returns null if the product is not storefront-accessible (must be `ACTIVE` status with `PUBLIC` or `BUNDLE_ONLY` visibility). |
172
172
  | `ProductConfigurator` | Fetches a product together with its filtered attribute definitions, optimized for the configurator UI (e.g. customer-facing text fields, finishing options, scoped variants). `fillingMode: "CUSTOMER"` returns only customer-facing attributes; pass `"BOTH"` to also include attributes shared with the merchant admin. Single round-trip — saves a separate `attributes` query. |
173
173
  | `Products` | Paginated product list (Relay Connection, default page size 20, max 100). The `query` argument supports a structured search syntax — `tag:summer`, `vendor:foo`, `product_type:shirts`, `variants.price:>10`, plus `AND`/`OR`/`NOT` — falling back to free-text title/content search. The `filters[]` array uses multi-filter logic: same field name appears multiple times → OR; different fields → AND. Sort: `RELEVANCE`, `TITLE`, `PRICE`, `NEWEST`, `OLDEST`, `BEST_SELLING`. The response includes a `filters` block for faceted navigation (counts per filterable attribute value). |
174
174
  | `ProductSearch` | Full-text product search — `$query` is required. Functionally equivalent to `Products` with `$query` set, minus the `sortKey` argument (search defaults to relevance ranking). Use for the search results page; combine with `filters[]` for guided refinement. |
@@ -185,7 +185,7 @@ full executable body of each operation.
185
185
 
186
186
  | Operation | Description |
187
187
  | --- | --- |
188
- | `Category` | Fetches a single category by `id` or `slug` with its parent and immediate children. Use for breadcrumbs and sub-navigation. Nested queries on `parent` / `children` are batched server-side — safe to use in lists without N+1 concerns. |
188
+ | `Category` | Fetches a single category by `id` or `handle` with its parent and immediate children. Use for breadcrumbs and sub-navigation. Nested queries on `parent` / `children` are batched server-side — safe to use in lists without N+1 concerns. |
189
189
  | `Categories` | Returns active root categories for the shop. Each root exposes its `children` — build the tree client-side by walking those fields (server batches the lookups, no N+1). The hierarchy is not depth-capped server-side. Use for nav mega-menus and category pages. |
190
190
 
191
191
  #### Cart
@@ -280,8 +280,8 @@ full executable body of each operation.
280
280
 
281
281
  | Operation | Description |
282
282
  | --- | --- |
283
- | `BlogPosts` | Paginated list of published blog posts. Filter by `categorySlug`, `tagSlug`, or `featured` (boolean flag, not enum). Sort: `PUBLISHED_AT` (default), `TITLE`, `VIEW_COUNT`, or `CREATED_AT`. Public; no auth required. |
284
- | `BlogPost` | Fetches a single blog post by `id` or `slug`. Visibility-gated: returns null if the post is not yet `PUBLISHED` or if its publish date is in the future (scheduled posts stay hidden until their publish time). Side effect: fetching a post increments its `view_count` asynchronously (does not block the response). |
283
+ | `BlogPosts` | Paginated list of published blog posts. Filter by `categoryHandle`, `tagHandle`, or `featured` (boolean flag, not enum). Sort: `PUBLISHED_AT` (default), `TITLE`, `VIEW_COUNT`, or `CREATED_AT`. Public; no auth required. |
284
+ | `BlogPost` | Fetches a single blog post by `id` or `handle`. Visibility-gated: returns null if the post is not yet `PUBLISHED` or if its publish date is in the future (scheduled posts stay hidden until their publish time). Side effect: fetching a post increments its `view_count` asynchronously (does not block the response). |
285
285
  | `BlogCategories` | Lists all blog categories with per-category `postCount` and SEO metadata. Use to render category navigation on blog pages. Public; no auth required. |
286
286
  | `BlogTags` | Lists blog tags with usage counts (`postCount` per tag). Use to render a tag cloud. Public; no auth required. |
287
287
 
@@ -302,7 +302,7 @@ full executable body of each operation.
302
302
 
303
303
  | Operation | Description |
304
304
  | --- | --- |
305
- | `Menu` | Fetches a navigation menu by `handle` (e.g. `"main-menu"`, `"footer"`, `"mobile"`). Returns the nested item tree. Each item is typed as one of: `HTTP`, `FRONTPAGE`, `SEARCH`, `CATALOG`, `BLOG`, `PRODUCT`, `COLLECTION`, `CATEGORY`, or `PAGE` — switch on the type to render the right link target. Linked resources and URLs are resolved on demand by the field selections in the `Menu` fragment. |
305
+ | `Menu` | Fetches a navigation menu by `handle` (e.g. `"main-menu"`, `"footer"`, `"mobile"`). Returns the nested item tree (up to 3 levels). Each item is typed as one of: `HTTP`, `FRONTPAGE`, `SEARCH`, `CATALOG`, `BLOG`, `PRODUCT`, `COLLECTION`, `CATEGORY`, `PAGE`, or `BRAND` — switch on the type to render the right link target. Each resource-linked item exposes both a pre-resolved `url` (standard `/categories\|/collections\|/pages\|/products\|/brands/<handle>` convention) and a typed `resource` union with the raw handle so storefronts with custom routing can construct their own paths instead. All resource lookups are batched per request — no N+1 even for deep menus. |
306
306
 
307
307
  #### Content: URL Redirects
308
308
 
@@ -459,7 +459,7 @@ full executable body of each operation.
459
459
 
460
460
  | Fragment | On Type | Description |
461
461
  | --- | --- | --- |
462
- | `Category` | `Category` | Category node in the shop's taxonomy — name, slug, hero image, depth `level`, materialized `path`, product count, sort order. Spread on category cards and breadcrumb segments. Does NOT include parent / children — query those fields separately and let the server batch them. |
462
+ | `Category` | `Category` | Category node in the shop's taxonomy — name, handle, hero image, depth `level`, materialized `path`, product count, sort order. Spread on category cards and breadcrumb segments. Does NOT include parent / children — query those fields separately and let the server batch them. |
463
463
 
464
464
  #### Customer
465
465
 
@@ -554,7 +554,7 @@ full executable body of each operation.
554
554
  | `AttributeFilterValue` | `AttributeFilterValue` | One discrete value in a filterable attribute (e.g. "Red" for the Color filter). Includes `productCount` in the current listing context (for "(12)" badges next to filter values), optional swatch and price modifier. |
555
555
  | `AttributeDefinition` | `AttributeDefinition` | Filterable attribute exposed on the storefront — name, type (SELECT / CHECKBOX / SLIDER / etc.), filterability flags, display order, and either discrete `filterValues[]` or numeric `rangeBounds`. Spread inside `availableFilters.attributes[]`. |
556
556
  | `PriceRangeFilter` | `PriceRange` | Min / max price range across products in the current listing context. Use to bound the price slider. |
557
- | `CategoryFilterOption` | `CategoryFilterOption` | One category option in the categories filter — id, name, slug, count of products in this category within the current listing context, level + parentId for tree rendering. |
557
+ | `CategoryFilterOption` | `CategoryFilterOption` | One category option in the categories filter — id, name, handle, count of products in this category within the current listing context, level + parentId for tree rendering. |
558
558
  | `AvailableFilters` | `AvailableFilters` | Full result of the `productFilters($input)` query — attribute filters, price range, category filters, brands, count of currently active filters, total products in context (Relay-aligned `totalCount`), and boolean facet count for `availableForSale` (`availableCount`). Spread on listing pages to render filter sidebars. Per-facet counts (`attributes[].filterValues[].productCount`, `brands[].productCount`, `categories[].productCount`, `availableCount`) use exclude-self aggregation: when a dimension is in `input.currentFilters` / `input.available`, its own facet count IGNORES that filter (shows "how many products would appear if this facet were toggled"), while OTHER dimensions APPLY their filters. This matches industry convention for accurate facet UX. |
559
559
 
560
560
  #### Loyalty Program
@@ -593,9 +593,9 @@ full executable body of each operation.
593
593
 
594
594
  | Fragment | On Type | Description |
595
595
  | --- | --- | --- |
596
- | `BlogCategory` | `BlogCategory` | Blog category — name, slug, description, post count. Spread on category navigation and post category labels. |
597
- | `BlogTag` | `BlogTag` | Blog tag — name, slug, post count. Spread on tag clouds and post tag labels. |
598
- | `BlogPost` | `BlogPost` | Full blog post — title, slug, excerpt, content (with `contentFormat` indicating HTML/Markdown), featured image, author, category, tags, publish date, reading time, view + comment counts, SEO metadata. Spread on the post detail page. |
596
+ | `BlogCategory` | `BlogCategory` | Blog category — name, handle, description, post count. Spread on category navigation and post category labels. |
597
+ | `BlogTag` | `BlogTag` | Blog tag — name, handle, post count. Spread on tag clouds and post tag labels. |
598
+ | `BlogPost` | `BlogPost` | Full blog post — title, handle, excerpt, content (with `contentFormat` indicating HTML/Markdown), featured image, author, category, tags, publish date, reading time, view + comment counts, SEO metadata. Spread on the post detail page. |
599
599
 
600
600
  #### Store Availability (BOPIS / multi-location)
601
601
 
@@ -613,13 +613,13 @@ full executable body of each operation.
613
613
 
614
614
  | Fragment | On Type | Description |
615
615
  | --- | --- | --- |
616
- | `ShopPage` | `ShopPage` | CMS page — handle (URL slug), title, body (HTML), excerpt, SEO metadata, publish date. Spread on About / Privacy / Terms / Returns Policy pages. |
616
+ | `ShopPage` | `ShopPage` | CMS page — handle (URL-friendly identifier), title, body (HTML), excerpt, SEO metadata, publish date. Spread on About / Privacy / Terms / Returns Policy pages. |
617
617
 
618
618
  #### Navigation Menus
619
619
 
620
620
  | Fragment | On Type | Description |
621
621
  | --- | --- | --- |
622
- | `MenuItem` | `MenuItem` | One menu item with up to 3 levels of nested children — title, URL, type (`HTTP` / `FRONTPAGE` / `SEARCH` / `CATALOG` / `BLOG` / `PRODUCT` / `COLLECTION` / `CATEGORY` / `PAGE`), `resourceId` for typed items, optional image. Switch on `type` to render the right link target. |
622
+ | `MenuItem` | `MenuItem` | One menu item with up to 3 levels of nested children — title, URL, type (`HTTP` / `FRONTPAGE` / `SEARCH` / `CATALOG` / `BLOG` / `PRODUCT` / `COLLECTION` / `CATEGORY` / `PAGE` / `BRAND`), `resourceId` for typed items, optional image, and a typed `resource` union (Category / Collection / ShopPage / Product / Brand) carrying the linked resource's handle. Two ways to render the link target: (1) use the pre-resolved `url` if your storefront follows the standard `/categories\|/collections\|/pages\|/products\|/brands/<handle>` route convention; (2) read `resource.__typename` + the per-type `handle` and build your own paths if your storefront uses different routing. Switch on `type` to decide which static (FRONTPAGE/SEARCH/CATALOG/BLOG) or dynamic target to render. |
623
623
  | `Menu` | `Menu` | Navigation menu — handle (e.g. "main-menu", "footer", "mobile"), title, nested item tree. Returned by `menu($handle)` query. |
624
624
 
625
625
  #### URL Redirects
package/fragments.graphql CHANGED
@@ -119,7 +119,7 @@ fragment ProductCard on Product {
119
119
  vendor
120
120
  categories {
121
121
  id
122
- slug
122
+ handle
123
123
  name
124
124
  }
125
125
  isAvailable
@@ -225,11 +225,11 @@ fragment Collection on Collection {
225
225
  # Categories
226
226
  # ============================================
227
227
 
228
- # Category node in the shop's taxonomy — name, slug, hero image, depth `level`, materialized `path`, product count, sort order. Spread on category cards and breadcrumb segments. Does NOT include parent / children — query those fields separately and let the server batch them.
228
+ # Category node in the shop's taxonomy — name, handle, hero image, depth `level`, materialized `path`, product count, sort order. Spread on category cards and breadcrumb segments. Does NOT include parent / children — query those fields separately and let the server batch them.
229
229
  fragment Category on Category {
230
230
  id
231
231
  name
232
- slug
232
+ handle
233
233
  description
234
234
  image {
235
235
  ...ImageCard
@@ -1004,11 +1004,11 @@ fragment PriceRangeFilter on PriceRange {
1004
1004
  }
1005
1005
  }
1006
1006
 
1007
- # One category option in the categories filter — id, name, slug, count of products in this category within the current listing context, level + parentId for tree rendering.
1007
+ # One category option in the categories filter — id, name, handle, count of products in this category within the current listing context, level + parentId for tree rendering.
1008
1008
  fragment CategoryFilterOption on CategoryFilterOption {
1009
1009
  id
1010
1010
  name
1011
- slug
1011
+ handle
1012
1012
  productCount
1013
1013
  level
1014
1014
  parentId
@@ -1121,7 +1121,7 @@ fragment LoyaltyTransaction on LoyaltyTransaction {
1121
1121
  fragment LoyaltyReward on LoyaltyReward {
1122
1122
  id
1123
1123
  name
1124
- slug
1124
+ handle
1125
1125
  type
1126
1126
  pointsCost
1127
1127
  discountPercent
@@ -1271,28 +1271,28 @@ fragment Wishlist on Wishlist {
1271
1271
  # Blog
1272
1272
  # ============================================
1273
1273
 
1274
- # Blog category — name, slug, description, post count. Spread on category navigation and post category labels.
1274
+ # Blog category — name, handle, description, post count. Spread on category navigation and post category labels.
1275
1275
  fragment BlogCategory on BlogCategory {
1276
1276
  id
1277
1277
  name
1278
- slug
1278
+ handle
1279
1279
  description
1280
1280
  postCount
1281
1281
  }
1282
1282
 
1283
- # Blog tag — name, slug, post count. Spread on tag clouds and post tag labels.
1283
+ # Blog tag — name, handle, post count. Spread on tag clouds and post tag labels.
1284
1284
  fragment BlogTag on BlogTag {
1285
1285
  id
1286
1286
  name
1287
- slug
1287
+ handle
1288
1288
  postCount
1289
1289
  }
1290
1290
 
1291
- # Full blog post — title, slug, excerpt, content (with `contentFormat` indicating HTML/Markdown), featured image, author, category, tags, publish date, reading time, view + comment counts, SEO metadata. Spread on the post detail page.
1291
+ # Full blog post — title, handle, excerpt, content (with `contentFormat` indicating HTML/Markdown), featured image, author, category, tags, publish date, reading time, view + comment counts, SEO metadata. Spread on the post detail page.
1292
1292
  fragment BlogPost on BlogPost {
1293
1293
  id
1294
1294
  title
1295
- slug
1295
+ handle
1296
1296
  excerpt
1297
1297
  content
1298
1298
  contentFormat
@@ -1427,7 +1427,7 @@ fragment VariantStoreAvailability on ProductVariant {
1427
1427
  # Pages
1428
1428
  # ============================================
1429
1429
 
1430
- # CMS page — handle (URL slug), title, body (HTML), excerpt, SEO metadata, publish date. Spread on About / Privacy / Terms / Returns Policy pages.
1430
+ # CMS page — handle (URL-friendly identifier), title, body (HTML), excerpt, SEO metadata, publish date. Spread on About / Privacy / Terms / Returns Policy pages.
1431
1431
  fragment ShopPage on ShopPage {
1432
1432
  id
1433
1433
  handle
@@ -1447,13 +1447,21 @@ fragment ShopPage on ShopPage {
1447
1447
  # Navigation Menus
1448
1448
  # ============================================
1449
1449
 
1450
- # One menu item with up to 3 levels of nested children — title, URL, type (`HTTP` / `FRONTPAGE` / `SEARCH` / `CATALOG` / `BLOG` / `PRODUCT` / `COLLECTION` / `CATEGORY` / `PAGE`), `resourceId` for typed items, optional image. Switch on `type` to render the right link target.
1450
+ # One menu item with up to 3 levels of nested children — title, URL, type (`HTTP` / `FRONTPAGE` / `SEARCH` / `CATALOG` / `BLOG` / `PRODUCT` / `COLLECTION` / `CATEGORY` / `PAGE` / `BRAND`), `resourceId` for typed items, optional image, and a typed `resource` union (Category / Collection / ShopPage / Product / Brand) carrying the linked resource's handle. Two ways to render the link target: (1) use the pre-resolved `url` if your storefront follows the standard `/categories|/collections|/pages|/products|/brands/<handle>` route convention; (2) read `resource.__typename` + the per-type `handle` and build your own paths if your storefront uses different routing. Switch on `type` to decide which static (FRONTPAGE/SEARCH/CATALOG/BLOG) or dynamic target to render.
1451
1451
  fragment MenuItem on MenuItem {
1452
1452
  id
1453
1453
  title
1454
1454
  url
1455
1455
  type
1456
1456
  resourceId
1457
+ resource {
1458
+ __typename
1459
+ ... on Category { id handle name }
1460
+ ... on Collection { id handle title }
1461
+ ... on ShopPage { id handle title }
1462
+ ... on Product { id handle title }
1463
+ ... on Brand { id handle name logo }
1464
+ }
1457
1465
  image {
1458
1466
  ...Image
1459
1467
  }
@@ -1463,12 +1471,28 @@ fragment MenuItem on MenuItem {
1463
1471
  url
1464
1472
  type
1465
1473
  resourceId
1474
+ resource {
1475
+ __typename
1476
+ ... on Category { id handle name }
1477
+ ... on Collection { id handle title }
1478
+ ... on ShopPage { id handle title }
1479
+ ... on Product { id handle title }
1480
+ ... on Brand { id handle name logo }
1481
+ }
1466
1482
  items {
1467
1483
  id
1468
1484
  title
1469
1485
  url
1470
1486
  type
1471
1487
  resourceId
1488
+ resource {
1489
+ __typename
1490
+ ... on Category { id handle name }
1491
+ ... on Collection { id handle title }
1492
+ ... on ShopPage { id handle title }
1493
+ ... on Product { id handle title }
1494
+ ... on Brand { id handle name logo }
1495
+ }
1472
1496
  }
1473
1497
  }
1474
1498
  }
@@ -1528,7 +1552,7 @@ fragment ProductAttributeOption on ProductAttributeOption {
1528
1552
  fragment ProductAttributeDefinition on ProductAttributeDefinition {
1529
1553
  id
1530
1554
  name
1531
- slug
1555
+ handle
1532
1556
  description
1533
1557
  type
1534
1558
  fillingMode