@scayle/storefront-product-listing 2.5.0 → 2.5.1

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,13 @@
1
1
  # @scayle/storefront-product-listing
2
2
 
3
+ ## 2.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - **\[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.
8
+
9
+ 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`.
10
+
3
11
  ## 2.5.0
4
12
 
5
13
  ### Minor Changes
@@ -12,7 +20,6 @@
12
20
 
13
21
  - Introduced support for Nuxt 4 while maintaining full backward compatibility with Nuxt 3.
14
22
  This enables consumers to migrate to Nuxt 4 ahead of the Nuxt 3 end of support on 31 Jan 2026.
15
-
16
23
  - **Version Requirements:**
17
24
  - Nuxt 3: `v3.13.0+`
18
25
  - Nuxt 4: `v4.2.0+`
@@ -46,7 +53,6 @@
46
53
  By removing href attributes from sort controls and using click handlers with programmatic navigation instead, we ensure that sorting doesn't create separate indexable pages, improving SEO performance.
47
54
 
48
55
  **Migration Required:** Replace usage of `sortLinks` with direct handling of `sortingOptions` and implement click handlers that use `router.push()` to update query parameters.
49
-
50
56
  - **Before:**
51
57
 
52
58
  ```ts
@@ -128,26 +134,25 @@
128
134
  - 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).
129
135
 
130
136
  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.
131
-
132
137
  - **Before:** Keys were static strings.
133
138
  - **After:** Keys can be reactive, enabling dynamic caching and automatic updates.
134
139
 
135
140
  ```ts
136
141
  // Before
137
- const { data } = await useRpc("getProduct", "my-static-product-key", {
142
+ const { data } = await useRpc('getProduct', 'my-static-product-key', {
138
143
  productId: 123,
139
- });
144
+ })
140
145
 
141
146
  // After
142
- const productId = ref(123);
147
+ const productId = ref(123)
143
148
 
144
149
  // The key is now reactive. If `productId` changes, the key updates
145
150
  // to 'product-456' (for example) and triggers a new fetch.
146
151
  const { data } = await useRpc(
147
- "getProduct",
152
+ 'getProduct',
148
153
  () => `product-${productId.value}`,
149
- { productId }
150
- );
154
+ { productId },
155
+ )
151
156
  ```
152
157
 
153
158
  ## 2.2.0
@@ -184,7 +189,6 @@
184
189
  - Added support for custom filter prefix for filter utility functions.
185
190
 
186
191
  Following functions now accept a `filterPrefix` parameter that can be used to customize the filter prefix.
187
-
188
192
  - `parseFilterDataFromRoute`
189
193
  - `createNewAttributeQuery`
190
194
  - `createNewPriceQuery`
@@ -227,14 +231,14 @@
227
231
  **Before**
228
232
 
229
233
  ```ts
230
- import { defaultSortingOptions } from "#storefront-product-listing";
234
+ import { defaultSortingOptions } from '#storefront-product-listing'
231
235
  const { sortLinks } = useProductListSort(useRoute(), {
232
236
  sortingOptions: defaultSortingOptions,
233
- });
237
+ })
234
238
  ```
235
239
 
236
240
  ```ts
237
- const { sortLinks } = useProductListSort(useRoute());
241
+ const { sortLinks } = useProductListSort(useRoute())
238
242
  ```
239
243
 
240
244
  (Both before approaches are equivalent.)
@@ -242,19 +246,19 @@
242
246
  **After**
243
247
 
244
248
  ```ts
245
- import { SORT_PRICE_ASC, SORT_PRICE_DESC } from "#storefront-product-listing";
249
+ import { SORT_PRICE_ASC, SORT_PRICE_DESC } from '#storefront-product-listing'
246
250
  const { sortLinks } = useProductListSort(useRoute(), {
247
251
  sortingOptions: [
248
252
  {
249
253
  ...SORT_PRICE_ASC,
250
- label: "Price Ascending",
254
+ label: 'Price Ascending',
251
255
  },
252
256
  {
253
257
  ...SORT_PRICE_DESC,
254
- label: "Price Descending",
258
+ label: 'Price Descending',
255
259
  },
256
260
  ],
257
- });
261
+ })
258
262
  ```
259
263
 
260
264
  - **BREAKING**: `useProductListingSeoData` now expects `isDefaultSortSelected` to be passed as the final parameter. This removes the internal dependency on `useProductListSort`. It is typed as `MaybeRefOrGetter<Boolean>` to support reactive usage.
@@ -262,12 +266,12 @@
262
266
  **Before**
263
267
 
264
268
  ```ts
265
- const route = useRoute();
269
+ const route = useRoute()
266
270
  const { title, robots, canonicalLink, categoryBreadcrumbSchema } =
267
271
  useProductListingSeoData(breadcrumbs, route, {
268
272
  baseUrl: baseUrl,
269
273
  fullPath: fullPath,
270
- });
274
+ })
271
275
  ```
272
276
 
273
277
  **After**
@@ -410,7 +414,6 @@
410
414
 
411
415
  - This release introduces the `@scayle/storefront-product-listing` package, decoupling composables and utilities of the Product Listing Page (PLP) functionality from the SCAYLE Storefront Boilerplate for enhanced modularity and integration.
412
416
  This separation empowers developers with greater control over PLP updates and simplifies its integration into existing Storefront-based projects.
413
-
414
417
  - [Discover more about the Product Listing Page for SCAYLE Storefront in our SCAYLE Resource Center.](https://scayle.dev/en/core-documentation/storefront-guide/storefront-application/features/product-listing-page)
415
418
 
416
419
  - 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`.
@@ -418,7 +421,6 @@
418
421
  ### Minor Changes
419
422
 
420
423
  - Compared to the original implementation within the SCAYLE Storefront Boilerplate, the `@scayle/storefront-product-detail` package has received some improvements and refactoring:
421
-
422
424
  - The `useAppliedFilters` composable has been added to the `storefront-product-listing` package.
423
425
  It now accepts a `filtersPrefix` parameter, allowing you to specify a custom prefix for filter-related query parameters.
424
426
  - The `useProductListSort` composable has been added to the `storefront-product-listing` package.
@@ -439,7 +441,7 @@
439
441
  They can be used as follows:
440
442
 
441
443
  ```ts
442
- import { filtersFactory } from "@scayle/storefront-product-listing";
444
+ import { filtersFactory } from '@scayle/storefront-product-listing'
443
445
  ```
444
446
 
445
447
  - Upgrade the `storefront-core` and `storefront-nuxt` packages to version 8 and update your implementation to reflect the changes in RPC composables:
package/dist/index.mjs CHANGED
@@ -7,8 +7,8 @@ const breadcrumbsFactory = Factory.define(() => [
7
7
  { to: "/de/c/women/clothing-2048", value: "clothing" }
8
8
  ]);
9
9
 
10
- const filtersFactory = Factory.define(
11
- () => [{
10
+ const filtersFactory = Factory.define(() => [
11
+ {
12
12
  id: 1,
13
13
  slug: "brand",
14
14
  name: "Brand",
@@ -22,7 +22,8 @@ const filtersFactory = Factory.define(
22
22
  ],
23
23
  type: FilterTypes.ATTRIBUTES,
24
24
  attributeGroupType: "group-type"
25
- }, {
25
+ },
26
+ {
26
27
  id: null,
27
28
  slug: "sale",
28
29
  name: "Sale",
@@ -37,7 +38,8 @@ const filtersFactory = Factory.define(
37
38
  }
38
39
  ],
39
40
  type: FilterTypes.BOOLEAN
40
- }, {
41
+ },
42
+ {
41
43
  id: null,
42
44
  slug: "prices",
43
45
  name: "Prices",
@@ -49,7 +51,7 @@ const filtersFactory = Factory.define(
49
51
  }
50
52
  ],
51
53
  type: FilterTypes.RANGE
52
- }]
53
- );
54
+ }
55
+ ]);
54
56
 
55
57
  export { breadcrumbsFactory, filtersFactory };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-listing",
3
3
  "configKey": "product-listing",
4
- "version": "2.5.0",
4
+ "version": "2.5.1",
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-listing";
4
- const PACKAGE_VERSION = "2.5.0";
4
+ const PACKAGE_VERSION = "2.5.1";
5
5
  const module$1 = defineNuxtModule({
6
6
  meta: {
7
7
  name: PACKAGE_NAME,
@@ -3,10 +3,5 @@ export function useAllShopCategoriesForId({
3
3
  params,
4
4
  options
5
5
  } = {}, key = "useAllShopCategoriesForId") {
6
- return useRpc(
7
- "getAllShopCategoriesForId",
8
- key,
9
- params,
10
- options
11
- );
6
+ return useRpc("getAllShopCategoriesForId", key, params, options);
12
7
  }
@@ -5,9 +5,13 @@ export function useAppliedFilters(route, { filtersPrefix } = { filtersPrefix: "f
5
5
  const parsedFilterQuery = computed(
6
6
  () => parseFilterDataFromRoute(route.query, filtersPrefix)
7
7
  );
8
- watch(() => JSON.stringify(parsedFilterQuery.value), () => {
9
- appliedFilter.value = parsedFilterQuery.value;
10
- }, { immediate: true });
8
+ watch(
9
+ () => JSON.stringify(parsedFilterQuery.value),
10
+ () => {
11
+ appliedFilter.value = parsedFilterQuery.value;
12
+ },
13
+ { immediate: true }
14
+ );
11
15
  const appliedFiltersCount = computed(() => {
12
16
  let count = 0;
13
17
  count += appliedFilter.value.attributes?.length ?? 0;
@@ -19,17 +23,15 @@ export function useAppliedFilters(route, { filtersPrefix } = { filtersPrefix: "f
19
23
  }
20
24
  return count;
21
25
  });
22
- const appliedAttributeValues = computed(
23
- () => {
24
- const attributes = appliedFilter.value?.attributes ?? [];
25
- return attributes.reduce((values, attribute) => {
26
- if (attribute.type === "attributes" && attribute.key) {
27
- values[attribute.key] = attribute.values;
28
- }
29
- return values;
30
- }, {});
31
- }
32
- );
26
+ const appliedAttributeValues = computed(() => {
27
+ const attributes = appliedFilter.value?.attributes ?? [];
28
+ return attributes.reduce((values, attribute) => {
29
+ if (attribute.type === "attributes" && attribute.key) {
30
+ values[attribute.key] = attribute.values;
31
+ }
32
+ return values;
33
+ }, {});
34
+ });
33
35
  const appliedBooleanValues = computed(() => {
34
36
  const attributes = appliedFilter.value?.attributes ?? [];
35
37
  return attributes.reduce((values, attribute) => {
@@ -16,12 +16,7 @@ export function useFiltersForListing({
16
16
  },
17
17
  fetchingKey ?? params?.value.categoryId ? `${params?.value.categoryId}-filters` : "search-filters"
18
18
  );
19
- const {
20
- data,
21
- refresh,
22
- status,
23
- error
24
- } = filterData;
19
+ const { data, refresh, status, error } = filterData;
25
20
  const availableFilters = computed(() => {
26
21
  return data.value?.filters.filter((filter) => {
27
22
  if (filter.type !== "boolean") {
@@ -39,11 +34,14 @@ export function useFiltersForListing({
39
34
  const filteredProductCount = computed(() => {
40
35
  return data.value?.unfilteredCount ?? 0;
41
36
  });
42
- return extendPromise(filterData.then(() => ({})), {
43
- refresh,
44
- status,
45
- error,
46
- availableFilters,
47
- filteredProductCount
48
- });
37
+ return extendPromise(
38
+ filterData.then(() => ({})),
39
+ {
40
+ refresh,
41
+ status,
42
+ error,
43
+ availableFilters,
44
+ filteredProductCount
45
+ }
46
+ );
49
47
  }
@@ -11,11 +11,13 @@ export function useProductListingSeoData(breadcrumbs, route, { baseUrl, fullPath
11
11
  return !areFiltersApplied.value && toValue(isDefaultSortSelected) && pageNumber === 1 ? "index,follow" : "noindex,follow";
12
12
  });
13
13
  const canonicalLink = computed(
14
- () => robots.value.includes("noindex") ? [] : [{
15
- rel: "canonical",
16
- key: "canonical",
17
- href: sanitizeCanonicalURL(`${baseUrl}${fullPath}`)
18
- }]
14
+ () => robots.value.includes("noindex") ? [] : [
15
+ {
16
+ rel: "canonical",
17
+ key: "canonical",
18
+ href: sanitizeCanonicalURL(`${baseUrl}${fullPath}`)
19
+ }
20
+ ]
19
21
  );
20
22
  const title = computed(() => {
21
23
  return toValue(breadcrumbs).map(({ value }) => value).join(" - ");
@@ -11,17 +11,20 @@ export function useProductsForListing({
11
11
  fetchingKey,
12
12
  fetchingOptions
13
13
  } = {}) {
14
- const productsData = useProducts({
15
- params: () => ({
16
- // NOTE: We don't want to include trackSearchAnalyticsEvent parameter when paginating,
17
- // as this logs the search term on every page request.
18
- // Instead, we log the term only once, when the search is first performed.
19
- // See: https://scayle.dev/en/core-documentation/the-basics/shops/search#scayle-search-analytics
20
- trackSearchAnalyticsEvent: !!(params.value.page === 1 && params.value?.where?.term),
21
- ...params.value
22
- }),
23
- options: fetchingOptions
24
- }, fetchingKey ?? `${params?.value?.categoryId ?? 0}-products`);
14
+ const productsData = useProducts(
15
+ {
16
+ params: () => ({
17
+ // NOTE: We don't want to include trackSearchAnalyticsEvent parameter when paginating,
18
+ // as this logs the search term on every page request.
19
+ // Instead, we log the term only once, when the search is first performed.
20
+ // See: https://scayle.dev/en/core-documentation/the-basics/shops/search#scayle-search-analytics
21
+ trackSearchAnalyticsEvent: !!(params.value.page === 1 && params.value?.where?.term),
22
+ ...params.value
23
+ }),
24
+ options: fetchingOptions
25
+ },
26
+ fetchingKey ?? `${params?.value?.categoryId ?? 0}-products`
27
+ );
25
28
  const products = computed(() => {
26
29
  return productsData.data.value?.products ?? [];
27
30
  });
@@ -4,44 +4,47 @@ import {
4
4
  HttpStatusMessage,
5
5
  defineRpcHandler
6
6
  } from "@scayle/storefront-nuxt";
7
- export const getAllShopCategoriesForId = defineRpcHandler(async ({ id, properties }, context) => {
8
- if (typeof id !== "number" || isNaN(id)) {
9
- return new ErrorResponse(
10
- HttpStatusCode.BAD_REQUEST,
11
- HttpStatusMessage.BAD_REQUEST,
12
- "Invalid category id"
13
- );
14
- }
15
- const { runtimeConfiguration, sapiClient, log, cached } = context;
16
- const fetchAllCategories = async () => {
17
- const categories = [];
18
- const shops = Object.values(
19
- runtimeConfiguration.storefront.shops
20
- );
21
- await Promise.all(
22
- shops.map(async (shop) => {
23
- try {
24
- const category = await sapiClient.clone({ shopId: shop.shopId }).categories.getById(id, {
25
- with: {
26
- properties
27
- }
28
- });
29
- categories.push({
30
- locale: shop.locale,
31
- path: Array.isArray(shop.path) ? shop.path[0] : shop.path,
32
- category
33
- });
34
- } catch (error) {
35
- log.error(error instanceof Error ? error : String(error));
36
- }
37
- })
38
- );
39
- return categories;
40
- };
41
- const result = await cached(fetchAllCategories, {
42
- cacheKeyPrefix: `getAllShopCategoriesForId-${id}`,
43
- ttl: 12 * 60 * 60
44
- // 12 hours
45
- })();
46
- return result;
47
- }, { method: "GET" });
7
+ export const getAllShopCategoriesForId = defineRpcHandler(
8
+ async ({ id, properties }, context) => {
9
+ if (typeof id !== "number" || isNaN(id)) {
10
+ return new ErrorResponse(
11
+ HttpStatusCode.BAD_REQUEST,
12
+ HttpStatusMessage.BAD_REQUEST,
13
+ "Invalid category id"
14
+ );
15
+ }
16
+ const { runtimeConfiguration, sapiClient, log, cached } = context;
17
+ const fetchAllCategories = async () => {
18
+ const categories = [];
19
+ const shops = Object.values(
20
+ runtimeConfiguration.storefront.shops
21
+ );
22
+ await Promise.all(
23
+ shops.map(async (shop) => {
24
+ try {
25
+ const category = await sapiClient.clone({ shopId: shop.shopId }).categories.getById(id, {
26
+ with: {
27
+ properties
28
+ }
29
+ });
30
+ categories.push({
31
+ locale: shop.locale,
32
+ path: Array.isArray(shop.path) ? shop.path[0] : shop.path,
33
+ category
34
+ });
35
+ } catch (error) {
36
+ log.error(error instanceof Error ? error : String(error));
37
+ }
38
+ })
39
+ );
40
+ return categories;
41
+ };
42
+ const result = await cached(fetchAllCategories, {
43
+ cacheKeyPrefix: `getAllShopCategoriesForId-${id}`,
44
+ ttl: 12 * 60 * 60
45
+ // 12 hours
46
+ })();
47
+ return result;
48
+ },
49
+ { method: "GET" }
50
+ );
@@ -9,10 +9,7 @@ export const parseFilterDataFromRoute = (query, filtersPrefix) => {
9
9
  if (!queryKey.includes(filtersPrefix)) {
10
10
  return;
11
11
  }
12
- const filterName = queryKey.replace(filtersPrefix, "").replace(
13
- /[\[\]]/g,
14
- ""
15
- );
12
+ const filterName = queryKey.replace(filtersPrefix, "").replace(/[\[\]]/g, "");
16
13
  switch (filterName) {
17
14
  case "minPrice":
18
15
  case "maxPrice":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-listing",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Collection of essential composables and utilities to work with product listing",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -44,29 +44,29 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@arethetypeswrong/cli": "0.18.2",
47
- "@nuxt/kit": "^3.20.2",
47
+ "@nuxt/kit": "^3.21.7",
48
48
  "@nuxt/module-builder": "1.0.2",
49
- "@nuxt/schema": "^3.20.2",
49
+ "@nuxt/schema": "^3.21.7",
50
50
  "@nuxt/test-utils": "4.0.0",
51
51
  "@scayle/eslint-config-storefront": "^4.8.0",
52
- "@types/node": "22.19.17",
53
- "@vitest/coverage-v8": "4.1.5",
54
- "@vueuse/core": "14.2.1",
52
+ "@types/node": "24.12.2",
53
+ "@vitest/coverage-v8": "4.1.9",
54
+ "@vueuse/core": "14.3.0",
55
55
  "eslint-formatter-gitlab": "7.1.0",
56
- "eslint": "10.2.1",
56
+ "eslint": "10.5.0",
57
57
  "fishery": "2.4.0",
58
58
  "h3": "1.15.11",
59
- "happy-dom": "20.9.0",
60
- "nuxt": "^3.20.2",
61
- "publint": "0.3.18",
59
+ "happy-dom": "20.10.6",
60
+ "nuxt": "^3.21.7",
61
+ "publint": "0.3.21",
62
62
  "schema-dts": "1.1.5",
63
- "typescript": "5.9.3",
63
+ "typescript": "6.0.3",
64
64
  "unbuild": "3.6.1",
65
- "vitest": "4.1.5",
65
+ "vitest": "4.1.9",
66
66
  "vue-router": "4.6.4",
67
- "vue-tsc": "3.2.6",
68
- "vue": "3.5.32",
69
- "@scayle/storefront-nuxt": "8.61.0",
67
+ "vue-tsc": "3.3.5",
68
+ "vue": "3.5.38",
69
+ "@scayle/storefront-nuxt": "8.62.1",
70
70
  "@scayle/vitest-config-storefront": "1.0.0"
71
71
  },
72
72
  "scripts": {