@scayle/storefront-product-detail 1.9.0 → 1.9.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @scayle/storefront-product-detail
2
2
 
3
+ ## 1.9.2
4
+
5
+ ### Patch Changes
6
+
7
+ - **\[SEO\]** Aligned `generateProductSchema` tests with the variant `referenceKey` mapped to schema.org `mpn` in product structured data after the `@scayle/storefront-api` 19.2.x price model update.
8
+
9
+ ## 1.9.1
10
+
11
+ ### Patch Changes
12
+
13
+ - **\[Security\]** Use the patched Nuxt for build — `nuxt@3.21.8` (3.x) / `nuxt@4.4.8` (4.x), with matching `@nuxt/kit` and `@nuxt/schema` — which includes the fix for [CVE-2026-53721](https://nvd.nist.gov/vuln/detail/CVE-2026-53721) ([GHSA-mm7m-92g8-7m47](https://github.com/nuxt/nuxt/security/advisories/GHSA-mm7m-92g8-7m47)), a route-rule middleware bypass.
14
+
15
+ Compatibility with previously-supported Nuxt versions is unchanged and remains specified via each package's `peerDependencies` and the Nuxt compatibility flag. `@scayle/storefront-core` is released in lockstep with `@scayle/storefront-nuxt`.
16
+
3
17
  ## 1.9.0
4
18
 
5
19
  ### Minor Changes
@@ -19,19 +33,19 @@
19
33
  params: {
20
34
  productId: 123,
21
35
  limit: 10,
22
- with: { attributes: "all", images: { attributes: "all" } },
36
+ with: { attributes: 'all', images: { attributes: 'all' } },
23
37
  where: {
24
38
  attributes: [
25
39
  {
26
- type: "attributes",
27
- key: "brand",
40
+ type: 'attributes',
41
+ key: 'brand',
28
42
  values: [456],
29
43
  },
30
44
  ],
31
45
  },
32
46
  ignoreSameMasterKey: true,
33
47
  },
34
- });
48
+ })
35
49
  ```
36
50
 
37
51
  ## 1.7.0
@@ -40,7 +54,6 @@
40
54
 
41
55
  - Introduced support for Nuxt 4 while maintaining full backward compatibility with Nuxt 3.
42
56
  This enables consumers to migrate to Nuxt 4 ahead of the Nuxt 3 end of support on 31 Jan 2026.
43
-
44
57
  - **Version Requirements:**
45
58
  - Nuxt 3: `v3.13.0+`
46
59
  - Nuxt 4: `v4.2.0+`
@@ -78,64 +91,64 @@
78
91
  **`sellableTimeframeStartsInFuture`:**
79
92
 
80
93
  ```ts
81
- import { sellableTimeframeStartsInFuture } from "@scayle/storefront-product-detail";
94
+ import { sellableTimeframeStartsInFuture } from '@scayle/storefront-product-detail'
82
95
 
83
96
  // Example timeframe
84
97
  const timeframe = {
85
- sellableFrom: "2099-01-01T00:00:00Z",
86
- sellableTo: "2100-01-01T00:00:00Z",
87
- };
98
+ sellableFrom: '2099-01-01T00:00:00Z',
99
+ sellableTo: '2100-01-01T00:00:00Z',
100
+ }
88
101
 
89
- const inFuture = sellableTimeframeStartsInFuture(timeframe);
90
- console.log(inFuture); // true if current date is before 2099-01-01
102
+ const inFuture = sellableTimeframeStartsInFuture(timeframe)
103
+ console.log(inFuture) // true if current date is before 2099-01-01
91
104
  ```
92
105
 
93
106
  **`sellableTimeframeEndsInPast`:**
94
107
 
95
108
  ```ts
96
- import { sellableTimeframeEndsInPast } from "@scayle/storefront-product-detail";
109
+ import { sellableTimeframeEndsInPast } from '@scayle/storefront-product-detail'
97
110
 
98
111
  const timeframe = {
99
- sellableFrom: "2018-01-01T00:00:00Z",
100
- sellableTo: "2020-01-01T00:00:00Z",
101
- };
112
+ sellableFrom: '2018-01-01T00:00:00Z',
113
+ sellableTo: '2020-01-01T00:00:00Z',
114
+ }
102
115
 
103
- const ended = sellableTimeframeEndsInPast(timeframe);
104
- console.log(ended); // true if current date is after 2020-01-01
116
+ const ended = sellableTimeframeEndsInPast(timeframe)
117
+ console.log(ended) // true if current date is after 2020-01-01
105
118
  ```
106
119
 
107
120
  **`isInSellableTimeframe`:**
108
121
 
109
122
  ```ts
110
- import { isInSellableTimeframe } from "@scayle/storefront-product-detail";
123
+ import { isInSellableTimeframe } from '@scayle/storefront-product-detail'
111
124
 
112
125
  const timeframe = {
113
- sellableFrom: "2023-01-01T00:00:00Z",
114
- sellableTo: "2025-01-01T00:00:00Z",
115
- };
126
+ sellableFrom: '2023-01-01T00:00:00Z',
127
+ sellableTo: '2025-01-01T00:00:00Z',
128
+ }
116
129
 
117
- const active = isInSellableTimeframe(timeframe);
118
- console.log(active); // true if current date is between 2023-01-01 and 2025-01-01
130
+ const active = isInSellableTimeframe(timeframe)
131
+ console.log(active) // true if current date is between 2023-01-01 and 2025-01-01
119
132
  ```
120
133
 
121
134
  **`useSellableTimeFrame`:**
122
135
 
123
136
  ```ts
124
- import { useSellableTimeFrame } from "@scayle/storefront-product-detail";
137
+ import { useSellableTimeFrame } from '@scayle/storefront-product-detail'
125
138
 
126
139
  const timeframe = {
127
- sellableFrom: "2023-01-01T00:00:00Z",
128
- sellableTo: "2025-01-01T00:00:00Z",
129
- };
140
+ sellableFrom: '2023-01-01T00:00:00Z',
141
+ sellableTo: '2025-01-01T00:00:00Z',
142
+ }
130
143
  const {
131
144
  isInSellableTimeframe,
132
145
  sellableTimeframeStartsInFuture,
133
146
  sellableTimeframeEndsInPast,
134
- } = useSellableTimeFrame(timeframe);
147
+ } = useSellableTimeFrame(timeframe)
135
148
 
136
- console.log(isInSellableTimeframe); // true if the timeframe is active
137
- console.log(sellableTimeframeStartsInFuture); // true if the time frame starts in the future
138
- console.log(sellableTimeframeEndsInPast); // true if the timeframe ended in the past
149
+ console.log(isInSellableTimeframe) // true if the timeframe is active
150
+ console.log(sellableTimeframeStartsInFuture) // true if the time frame starts in the future
151
+ console.log(sellableTimeframeEndsInPast) // true if the timeframe ended in the past
139
152
  ```
140
153
 
141
154
  ## 1.5.6
@@ -145,26 +158,25 @@
145
158
  - Enhanced data fetching composables to support reactive keys, aligning with [Nuxt 3.17 data fetching improvements](https://nuxt.com/blog/v3-17#data-fetching-improvements).
146
159
 
147
160
  You can now pass a `ref`, `computed`, or getter function as the `key` parameter to `useRpc` and derived composables (like `useProducts`, `useBrand` etc.). When the reactive key changes, the data will automatically re-fetch.
148
-
149
161
  - **Before:** Keys were static strings.
150
162
  - **After:** Keys can be reactive, enabling dynamic caching and automatic updates.
151
163
 
152
164
  ```ts
153
165
  // Before
154
- const { data } = await useRpc("getProduct", "my-static-product-key", {
166
+ const { data } = await useRpc('getProduct', 'my-static-product-key', {
155
167
  productId: 123,
156
- });
168
+ })
157
169
 
158
170
  // After
159
- const productId = ref(123);
171
+ const productId = ref(123)
160
172
 
161
173
  // The key is now reactive. If `productId` changes, the key updates
162
174
  // to 'product-456' (for example) and triggers a new fetch.
163
175
  const { data } = await useRpc(
164
- "getProduct",
176
+ 'getProduct',
165
177
  () => `product-${productId.value}`,
166
- { productId }
167
- );
178
+ { productId },
179
+ )
168
180
  ```
169
181
 
170
182
  ## 1.5.5
@@ -201,15 +213,15 @@
201
213
 
202
214
  ```ts
203
215
  export default defineNuxtConfig({
204
- modules: ["@scayle/storefront-product-detail"],
216
+ modules: ['@scayle/storefront-product-detail'],
205
217
  runtimeConfig: {
206
218
  public: {
207
- "product-detail": {
219
+ 'product-detail': {
208
220
  maxRecentlyViewedProducts: 15,
209
221
  },
210
222
  },
211
223
  },
212
- });
224
+ })
213
225
  ```
214
226
 
215
227
  ## 1.5.0
@@ -221,15 +233,15 @@
221
233
  Example usage:
222
234
 
223
235
  ```ts
224
- import { useRecentlyViewedProducts } from "#storefront-product-detail/composables";
236
+ import { useRecentlyViewedProducts } from '#storefront-product-detail/composables'
225
237
 
226
238
  const { products, addProduct, loadMissingProducts } =
227
- useRecentlyViewedProducts();
239
+ useRecentlyViewedProducts()
228
240
 
229
241
  onBeforeMount(async () => {
230
- addProductId(1234);
231
- await loadMissingProducts();
232
- });
242
+ addProductId(1234)
243
+ await loadMissingProducts()
244
+ })
233
245
  ```
234
246
 
235
247
  ## 1.4.2
@@ -261,44 +273,44 @@
261
273
 
262
274
  ```ts
263
275
  useProductSeoData({
264
- name: "Name",
265
- brand: "Brand",
266
- productDescription: "Description",
276
+ name: 'Name',
277
+ brand: 'Brand',
278
+ productDescription: 'Description',
267
279
  variants: variants.value.map((variant) => {
268
280
  return generateProductSchema({
269
- productName: "Name",
281
+ productName: 'Name',
270
282
  variant,
271
283
  url: `${$config.public.baseUrl}${route.fullPath}`,
272
284
  size,
273
- });
285
+ })
274
286
  }),
275
287
  images: images.value,
276
288
  productId: product.value?.id || 0,
277
289
  color: formatColors(colors.value),
278
290
  variesBy:
279
- variants.value.length > 1 ? ["https://schema.org/size"] : undefined,
280
- });
291
+ variants.value.length > 1 ? ['https://schema.org/size'] : undefined,
292
+ })
281
293
 
282
294
  // Will become
283
295
 
284
296
  useProductSeoData({
285
- name: "Name",
286
- brand: "Brand",
287
- productDescription: "Description",
297
+ name: 'Name',
298
+ brand: 'Brand',
299
+ productDescription: 'Description',
288
300
  variants: variants.value.map((variant) => {
289
301
  return generateProductSchema({
290
- productName: "Name",
302
+ productName: 'Name',
291
303
  variant,
292
304
  url: `${$config.public.baseUrl}${route.fullPath}`,
293
305
  size,
294
306
  images: images.value[0],
295
- });
307
+ })
296
308
  }),
297
309
  productId: product.value?.id || 0,
298
310
  color: formatColors(colors.value),
299
311
  variesBy:
300
- variants.value.length > 1 ? ["https://schema.org/size"] : undefined,
301
- });
312
+ variants.value.length > 1 ? ['https://schema.org/size'] : undefined,
313
+ })
302
314
  ```
303
315
 
304
316
  ## 1.2.0
@@ -355,7 +367,6 @@
355
367
 
356
368
  - This release introduces the `@scayle/storefront-product-detail` package, decoupling composables and utilities of the Product Detail Page (PDP) functionality from the SCAYLE Storefront Boilerplate for enhanced modularity and integration.
357
369
  This separation empowers developers with greater control over PDP updates and simplifies its integration into existing Storefront-based projects.
358
-
359
370
  - [Discover more about the Product Detail Page for SCAYLE Storefront in our SCAYLE Resource Center.](https://scayle.dev/en/core-documentation/storefront-guide/storefront-application/features/product-detail-page)
360
371
 
361
372
  - This release requires `@scayle/storefront-nuxt@8.x` or higher. Support for `@scayle/storefront-nuxt@7.x` has been discontinued. Please update your dependencies accordingly. The `peerDependency` range has been updated to `^8.0.0`.
@@ -363,7 +374,6 @@
363
374
  ### Minor Changes
364
375
 
365
376
  - Compared to the original implementation within the SCAYLE Storefront Boilerplate, the `@scayle/storefront-product-detail` package has received some improvements and refactoring:
366
-
367
377
  - Introduced the `getCombineWithProductIds` function within the product utility, enabling efficient retrieval of product combinations within product listings.
368
378
  - Added the `getFilteredAttributeGroups` function to the attribute utility, simplifying the retrieval and formatting of filtered product attributes for display in listings.
369
379
  - Introduced the `useProductSeoData` composable, automating the generation of SEO-critical metadata for product pages. This includes dynamically generated canonical links, robots meta tags, product titles, and structured data (`JSON-LD`) for product details and breadcrumbs, enhancing search engine visibility.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
3
  "configKey": "product-detail",
4
- "version": "1.9.0",
4
+ "version": "1.9.2",
5
5
  "compatibility": {
6
6
  "bridge": false,
7
7
  "nuxt": ">=3.13"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, addImportsDir } from '@nuxt/kit';
2
2
 
3
3
  const PACKAGE_NAME = "@scayle/storefront-product-detail";
4
- const PACKAGE_VERSION = "1.9.0";
4
+ const PACKAGE_VERSION = "1.9.2";
5
5
  const module$1 = defineNuxtModule({
6
6
  meta: {
7
7
  name: PACKAGE_NAME,
@@ -3,10 +3,5 @@ export function useAllShopProductsForId({
3
3
  params,
4
4
  options
5
5
  } = {}, key = "useAllShopProductsById") {
6
- return useRpc(
7
- "getAllShopProductsForId",
8
- key,
9
- params,
10
- options
11
- );
6
+ return useRpc("getAllShopProductsForId", key, params, options);
12
7
  }
@@ -65,9 +65,7 @@ export function useRecentlyViewedProducts(options) {
65
65
  products.value.push(...result);
66
66
  }
67
67
  const sortOrderMap = new Map(
68
- recentlyViewedProductsIds.value.map(
69
- (id, index) => [id, index]
70
- )
68
+ recentlyViewedProductsIds.value.map((id, index) => [id, index])
71
69
  );
72
70
  products.value = products.value.sort((a, b) => {
73
71
  const aIndex = sortOrderMap.get(a.id);
@@ -1,33 +1,36 @@
1
1
  import { defineRpcHandler } from "@scayle/storefront-nuxt";
2
- export const getAllShopProductsForId = defineRpcHandler(async (options, context) => {
3
- const { runtimeConfiguration, sapiClient, log, cached } = context;
4
- const fetchAllProducts = async () => {
5
- const products = [];
6
- const shops = Object.values(
7
- runtimeConfiguration.storefront.shops
8
- );
9
- await Promise.all(
10
- shops.map(async (shop) => {
11
- try {
12
- const product = await sapiClient.clone({ shopId: shop.shopId }).products.getById(options.id, {
13
- with: options.with
14
- });
15
- products.push({
16
- locale: shop.locale,
17
- path: Array.isArray(shop.path) ? shop.path[0] : shop.path,
18
- product
19
- });
20
- } catch (error) {
21
- log.error(error instanceof Error ? error : String(error));
22
- }
23
- })
24
- );
25
- return products;
26
- };
27
- const result = await cached(fetchAllProducts, {
28
- cacheKeyPrefix: `getAllShopProductsForId-${options.id}`,
29
- ttl: 12 * 60 * 60
30
- // 12 hours
31
- })();
32
- return result;
33
- }, { method: "GET" });
2
+ export const getAllShopProductsForId = defineRpcHandler(
3
+ async (options, context) => {
4
+ const { runtimeConfiguration, sapiClient, log, cached } = context;
5
+ const fetchAllProducts = async () => {
6
+ const products = [];
7
+ const shops = Object.values(
8
+ runtimeConfiguration.storefront.shops
9
+ );
10
+ await Promise.all(
11
+ shops.map(async (shop) => {
12
+ try {
13
+ const product = await sapiClient.clone({ shopId: shop.shopId }).products.getById(options.id, {
14
+ with: options.with
15
+ });
16
+ products.push({
17
+ locale: shop.locale,
18
+ path: Array.isArray(shop.path) ? shop.path[0] : shop.path,
19
+ product
20
+ });
21
+ } catch (error) {
22
+ log.error(error instanceof Error ? error : String(error));
23
+ }
24
+ })
25
+ );
26
+ return products;
27
+ };
28
+ const result = await cached(fetchAllProducts, {
29
+ cacheKeyPrefix: `getAllShopProductsForId-${options.id}`,
30
+ ttl: 12 * 60 * 60
31
+ // 12 hours
32
+ })();
33
+ return result;
34
+ },
35
+ { method: "GET" }
36
+ );
@@ -5,34 +5,34 @@ import {
5
5
  MIN_WITH_PARAMS_PRODUCT,
6
6
  defineRpcHandler
7
7
  } from "@scayle/storefront-nuxt";
8
- export const getSimilarProducts = defineRpcHandler(async (options, context) => {
9
- const { sapiClient, cached, withParams } = context;
10
- const campaignKey = await context.callRpc?.("getCampaignKey");
11
- const { productId, with: withParameter, ...rest } = options;
12
- const fetch = async (productId2, params) => {
13
- try {
14
- return sapiClient.recommendations.getSimilarProducts(productId2, params);
15
- } catch (e) {
16
- if (e instanceof FetchError) {
17
- const response = e.response;
18
- return new ErrorResponse(
19
- response.status,
20
- response.statusText,
21
- "Failed to fetch similar products"
22
- );
8
+ export const getSimilarProducts = defineRpcHandler(
9
+ async (options, context) => {
10
+ const { sapiClient, cached, withParams } = context;
11
+ const campaignKey = await context.callRpc?.("getCampaignKey");
12
+ const { productId, with: withParameter, ...rest } = options;
13
+ const fetch = async (productId2, params) => {
14
+ try {
15
+ return sapiClient.recommendations.getSimilarProducts(productId2, params);
16
+ } catch (e) {
17
+ if (e instanceof FetchError) {
18
+ const response = e.response;
19
+ return new ErrorResponse(
20
+ response.status,
21
+ response.statusText,
22
+ "Failed to fetch similar products"
23
+ );
24
+ }
25
+ throw e;
23
26
  }
24
- throw e;
25
- }
26
- };
27
- return await cached(
28
- fetch,
29
- {
27
+ };
28
+ return await cached(fetch, {
30
29
  cacheKeyPrefix: "getSimilarProducts-products",
31
30
  ttl: 5 * MINUTE
32
- }
33
- )(productId, {
34
- with: withParameter ?? withParams?.product ?? MIN_WITH_PARAMS_PRODUCT,
35
- campaignKey,
36
- ...rest
37
- });
38
- }, { method: "GET" });
31
+ })(productId, {
32
+ with: withParameter ?? withParams?.product ?? MIN_WITH_PARAMS_PRODUCT,
33
+ campaignKey,
34
+ ...rest
35
+ });
36
+ },
37
+ { method: "GET" }
38
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Collection of essential composables and utilities to work with product detail",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -40,27 +40,27 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@arethetypeswrong/cli": "0.18.2",
43
- "@nuxt/kit": "^3.20.2",
43
+ "@nuxt/kit": "^3.21.7",
44
44
  "@nuxt/module-builder": "1.0.2",
45
- "@nuxt/schema": "^3.20.2",
45
+ "@nuxt/schema": "^3.21.7",
46
46
  "@nuxt/test-utils": "4.0.0",
47
47
  "@scayle/eslint-config-storefront": "^4.8.0",
48
- "@types/node": "22.19.17",
49
- "@vitest/coverage-v8": "4.1.5",
50
- "@vueuse/core": "14.2.1",
51
- "eslint-formatter-gitlab": "7.1.0",
52
- "eslint": "10.2.1",
53
- "happy-dom": "20.9.0",
54
- "nuxt": "^3.20.2",
55
- "publint": "0.3.18",
48
+ "@types/node": "24.12.2",
49
+ "@vitest/coverage-v8": "4.1.9",
50
+ "@vueuse/core": "14.3.0",
51
+ "eslint-formatter-gitlab": "7.2.0",
52
+ "eslint": "10.7.0",
53
+ "happy-dom": "20.10.6",
54
+ "nuxt": "^3.21.7",
55
+ "publint": "0.3.21",
56
56
  "schema-dts": "1.1.5",
57
- "typescript": "5.9.3",
57
+ "typescript": "6.0.3",
58
58
  "unbuild": "3.6.1",
59
- "vitest": "4.1.5",
59
+ "vitest": "4.1.10",
60
60
  "vue-router": "4.6.4",
61
- "vue-tsc": "3.2.6",
62
- "vue": "3.5.32",
63
- "@scayle/storefront-nuxt": "8.61.0",
61
+ "vue-tsc": "3.3.7",
62
+ "vue": "3.5.39",
63
+ "@scayle/storefront-nuxt": "8.62.3",
64
64
  "@scayle/vitest-config-storefront": "1.0.0"
65
65
  },
66
66
  "scripts": {