@shopware/cms-base-layer 3.0.1 → 4.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.
Files changed (52) hide show
  1. package/README.md +78 -15
  2. package/app/app.config.ts +3 -3
  3. package/app/assets/css/rich-text.css +80 -0
  4. package/app/components/SwContactForm.vue +2 -1
  5. package/app/components/SwFilterChips.vue +29 -7
  6. package/app/components/SwNewsletterForm.vue +2 -1
  7. package/app/components/SwProductCard.vue +19 -3
  8. package/app/components/SwProductCardImage.vue +5 -1
  9. package/app/components/SwProductListingFilter.vue +5 -0
  10. package/app/components/SwProductListingFilters.vue +76 -74
  11. package/app/components/SwProductListingFiltersHorizontal.vue +73 -72
  12. package/app/components/SwStockInfo.vue +5 -2
  13. package/app/components/SwVariantConfigurator.vue +8 -3
  14. package/app/components/listing-filters/SwFilterCategories.vue +158 -0
  15. package/app/components/public/cms/CmsGenericBlock.vue +1 -1
  16. package/app/components/public/cms/CmsGenericElement.vue +1 -1
  17. package/app/components/public/cms/CmsNoComponent.vue +1 -1
  18. package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +1 -1
  19. package/app/components/public/cms/block/CmsBlockCenterText.vue +2 -1
  20. package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +5 -1
  21. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +6 -2
  22. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +9 -5
  23. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +7 -2
  24. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +9 -3
  25. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +7 -2
  26. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +7 -3
  27. package/app/components/public/cms/block/CmsBlockProductHeading.vue +2 -1
  28. package/app/components/public/cms/element/CmsElementBuyBox.vue +4 -1
  29. package/app/components/public/cms/element/CmsElementCrossSelling.vue +2 -1
  30. package/app/components/public/cms/element/CmsElementHtml.vue +1 -1
  31. package/app/components/public/cms/element/CmsElementImage.vue +41 -2
  32. package/app/components/public/cms/element/CmsElementImageGallery.vue +2 -1
  33. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +18 -9
  34. package/app/components/public/cms/element/CmsElementText.md +77 -0
  35. package/app/components/public/cms/element/CmsElementText.vue +5 -32
  36. package/app/helpers/html-to-vue/renderToHtml.test.ts +55 -0
  37. package/app/helpers/html-to-vue/renderToHtml.ts +5 -1
  38. package/app/helpers/html-to-vue/renderer.ts +10 -1
  39. package/app/utils/listingFilters.test.ts +39 -0
  40. package/app/utils/listingFilters.ts +19 -0
  41. package/app/utils/routeQuery.test.ts +65 -0
  42. package/app/utils/routeQuery.ts +26 -0
  43. package/app/utils/useSelectedListingFilters.test.ts +92 -0
  44. package/app/utils/useSelectedListingFilters.ts +111 -0
  45. package/component-dirs.json +20 -0
  46. package/index.d.ts +2 -2
  47. package/nuxt.config.ts +35 -29
  48. package/package.json +27 -31
  49. package/dist/index.d.mts +0 -5
  50. package/dist/index.d.ts +0 -5
  51. package/dist/index.mjs +0 -31
  52. package/index.cjs +0 -7
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![](https://img.shields.io/github/issues/shopware/frontends/cms-base?label=cms-base%20issues&logo=github)](https://github.com/shopware/frontends/issues?q=is%3Aopen+is%3Aissue+label%3Acms-base)
6
6
  [![](https://img.shields.io/github/license/shopware/frontends?color=blue)](#)
7
7
 
8
- Nuxt [layer](https://nuxt.com/docs/getting-started/layers) that provides an implementation of all CMS components in Shopware [based on utility-classes](https://frontends.shopware.com/framework/styling.html).
8
+ Nuxt [layer](https://nuxt.com/docs/getting-started/layers) that provides an implementation of all CMS components in Shopware [based on utility-classes](https://developer.shopware.com/frontends/concepts/styling.html).
9
9
 
10
10
  It is useful for projects that want to use the CMS components while keeping CMS functionality separate from the styling system and design tokens.
11
11
 
@@ -90,7 +90,7 @@ Since all CMS components are registered in your Nuxt application, you can now st
90
90
 
91
91
  > `@shopware/cms-base-layer` no longer owns the default UnoCSS theme. If you want the shared Shopware Frontends design tokens and UnoCSS defaults, extend `@shopware/unocss-design-tokens-layer` as shown above.
92
92
 
93
- See a [short guide](https://frontends.shopware.com/getting-started/cms/content-pages.html#use-the-cms-base-package) on how to use `cms-base-layer` in your Nuxt project.
93
+ See a [short guide](https://developer.shopware.com/frontends/guides/cms/content-pages.html#use-the-cms-base-package) on how to use `cms-base-layer` in your Nuxt project.
94
94
 
95
95
  ## Styling and Design Tokens
96
96
 
@@ -192,10 +192,18 @@ Replace standard `<img>` tags with `<NuxtImg>` to enable automatic optimization:
192
192
  :src="product.cover.media.url"
193
193
  preset="productDetail"
194
194
  :width="800"
195
- :alt="product.cover.media.alt"
195
+ :alt="getTranslatedProperty(product.cover.media, 'alt')"
196
196
  />
197
197
  ```
198
198
 
199
+ The media `alt` text is a translatable field, so read it through `getTranslatedProperty` rather than
200
+ `media.alt` — the latter always holds the system language value, no matter which language the current
201
+ request asks for. The helper is not auto-imported, so add it to the component's script block:
202
+
203
+ ```ts
204
+ import { getTranslatedProperty } from "@shopware/helpers";
205
+ ```
206
+
199
207
  ### Supported Modifiers
200
208
 
201
209
  Shopware supports the following URL parameters for image transformation:
@@ -350,7 +358,7 @@ url("https://cdn.shopware.store/.../image.jpg?width=1000&fit=crop,smart&format=w
350
358
 
351
359
  ## LCP Image Preload
352
360
 
353
- This layer includes a `useLcpImagePreload` composable that automatically preloads the first image found in CMS page content. This targets the [Largest Contentful Paint (LCP)](https://web.dev/lcp/) element, which is often a hero background image or the first visible image element.
361
+ This layer includes a `useLcpImagePreload` composable that preloads the first image found in CMS page content when enabled via `lcpImagePreload: true` in your `app.config.ts` (disabled by default). This targets the [Largest Contentful Paint (LCP)](https://web.dev/lcp/) element, which is often a hero background image or the first visible image element.
354
362
 
355
363
  ### How it works
356
364
 
@@ -364,12 +372,10 @@ The first image found is injected as a `<link rel="preload" as="image" fetchprio
364
372
 
365
373
  ### Usage
366
374
 
367
- The composable is already called in `CmsPage.vue`. If you override `CmsPage`, you can use it in your custom component:
375
+ The composable is already called in `CmsPage.vue`. If you override `CmsPage`, you can use it in your custom component. It is auto-imported once your app extends this layer, so it needs no import statement:
368
376
 
369
377
  ```vue
370
378
  <script setup>
371
- import { useLcpImagePreload } from "@shopware/cms-base-layer/composables/useLcpImagePreload";
372
-
373
379
  const props = defineProps<{ content: Schemas["CmsPage"] }>();
374
380
 
375
381
  useLcpImagePreload(props.content?.sections || []);
@@ -384,7 +390,7 @@ Images are optimized to prevent the browser from downloading images larger than
384
390
 
385
391
  ### Product Card Images (`SwProductCardImage`)
386
392
 
387
- The `productCard` preset only defines URL modifiers (format/quality/fit). `width`/`height`/`densities`/`loading` stay on the component — NuxtImg presets don't propagate these reliably:
393
+ The `productCard` preset only defines URL modifiers (format/quality/fit). `width`/`height` and `loading` stay on the component: in `@nuxt/image` 2.1.0 a preset carries `width`/`height` only as `modifiers`, which shape the URL rather than the rendered attributes, and `loading` is not a preset field at all. `densities` is kept alongside them for consistency, though a preset would propagate it:
388
394
 
389
395
  ```ts
390
396
  // nuxt.config.ts
@@ -442,15 +448,15 @@ export default defineAppConfig({
442
448
 
443
449
  ## 📘 Available components
444
450
 
445
- The list of available blocks and elements is [here](https://frontends.shopware.com/packages/cms-base-layer.html#available-components).
451
+ The list of available blocks and elements is [here](https://developer.shopware.com/frontends/packages/cms-base-layer.html#available-components).
446
452
 
447
453
  ## 🔄 Overwriting components
448
454
 
449
455
  The procedure is:
450
456
 
451
- - find a component in component's [list](https://frontends.shopware.com/packages/cms-base.html#available-components), using a [Vue devtools](https://devtools.vuejs.org/) or browsing the github [repository](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer/app/components)
457
+ - find a component in component's [list](https://developer.shopware.com/frontends/packages/cms-base-layer.html#available-components), using a [Vue devtools](https://devtools.vuejs.org/) or browsing the github [repository](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer/app/components)
452
458
  - take its name
453
- - create a file with the same name and place it into `~/components` dir in your nuxt project (or wherever according your nuxt config)
459
+ - create a file with the same name and place it under a `components` directory that your `nuxt.config.ts` registers with `global: true` — CMS components are looked up with `resolveComponent`, so an override outside a global path is never found and this layer's version keeps rendering with no error. In the starter template that directory is `app/components/cms/`; because it is registered with `pathPrefix: false`, the name comes from the filename alone and subdirectory depth under it does not matter.
454
460
 
455
461
  ✅ Thanks to this, nuxt will take the component registered in your app instead of the one registered by this nuxt layer.
456
462
 
@@ -475,7 +481,7 @@ No additional packages needed to be installed.
475
481
 
476
482
  ## Links
477
483
 
478
- - [📘 Documentation](https://frontends.shopware.com)
484
+ - [📘 Documentation](https://developer.shopware.com/frontends)
479
485
 
480
486
  - [👥 Community](https://discord.com/channels/1308047705309708348/1405501315160739951) (`#composable-frontend`)
481
487
 
@@ -485,9 +491,66 @@ No additional packages needed to be installed.
485
491
 
486
492
  Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
487
493
 
488
- ### Latest changes: 3.0.1
494
+ ### Latest changes: 4.0.0
495
+
496
+ ### Major Changes
497
+
498
+ - [#2609](https://github.com/shopware/frontends/pull/2609) [`2dc86da`](https://github.com/shopware/frontends/commit/2dc86da2532e986ebdadd06a922e68403414a0ff) Thanks [@mkucmus](https://github.com/mkucmus)! - Make the package a plain Nuxt layer.
499
+
500
+ **Breaking:** the package now declares an `exports` map, so only the layer entry point
501
+ is importable. Deep imports such as
502
+ `@shopware/cms-base-layer/app/components/SwProductCard.vue` no longer resolve. Use the
503
+ auto-registered global components instead, which is what extending the layer gives you.
504
+
505
+ **Breaking:** the leftover Nuxt module is gone, so `index.cjs` and the `dist` build are
506
+ no longer published. The module registered a component folder that moved to `app/` a
507
+ while ago, so it could not work any more. Consume this package with `extends`, which
508
+ resolves `nuxt.config.ts`:
509
+
510
+ ```ts
511
+ export default defineNuxtConfig({
512
+ extends: ["@shopware/cms-base-layer"],
513
+ });
514
+ ```
515
+
516
+ Other changes:
517
+
518
+ - Dropped the `build` and `dev` scripts and the `unbuild` dev dependency. There is
519
+ nothing left to bundle.
520
+ - Fixed `files`. It listed `helpers` and `app.config.ts`, which do not exist at the
521
+ package root, and shipped the dead `dist` and `index.cjs`.
522
+ - Deleted `components.d.ts`. It typed two components at paths that moved to
523
+ `app/components/public/cms/`, and no tsconfig referenced the file.
524
+ - Removed two vitest aliases that pointed at files which do not exist.
525
+ - Removed the `check-colors` script. It read `uno.config.ts` from this package, which
526
+ moved to `@shopware/unocss-design-tokens-layer`, so it always found zero colors. It
527
+ also relied on `tsx` without declaring it.
528
+
529
+ ### Minor Changes
530
+
531
+ - [#2574](https://github.com/shopware/frontends/pull/2574) [`2ddf156`](https://github.com/shopware/frontends/commit/2ddf156805b2941fe2069e78453fb3c4eb6d44ac) Thanks [@mkucmus](https://github.com/mkucmus)! - `SwProductListingFilters` and `SwProductListingFiltersHorizontal` render a new `SwFilterCategories` checkbox filter when the listing response contains the category aggregations from `getCategoryFilterAggregations()`. The selection is kept in the `categories` URL param and applied as a criteria `post-filter`. Both are search listings only (`listing-type="productSearchListing"`); on any other listing the category filter is not rendered, because the selection would neither reach the URL nor the criteria. Listings without these aggregations are unaffected.
489
532
 
490
533
  ### Patch Changes
491
534
 
492
- - Updated dependencies [[`8be060d`](https://github.com/shopware/frontends/commit/8be060de825ca799f98a8f045a5e7fea61f5d1a2), [`5678fb0`](https://github.com/shopware/frontends/commit/5678fb008cbd86eaddd061e004de89e6f45bb7ec), [`22611e5`](https://github.com/shopware/frontends/commit/22611e542b8f42a4f34dce5186f628f9a17f457b)]:
493
- - @shopware/composables@1.12.0
535
+ - [#2645](https://github.com/shopware/frontends/pull/2645) [`7a7ce3e`](https://github.com/shopware/frontends/commit/7a7ce3e5649e7ab84f7e547ce1ebcd9413117dde) Thanks [@mkucmus](https://github.com/mkucmus)! - Ship `component-dirs.json` in the published package. `nuxt.config.ts` imports it at load time, but it was missing from `files`, so the layer failed to load when installed from the registry.
536
+
537
+ - [#2660](https://github.com/shopware/frontends/pull/2660) [`8913956`](https://github.com/shopware/frontends/commit/89139563924163e57cafdd9770fe603f2dbd8cba) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Read translatable entity fields through their `translated` object instead of the untranslated entity root, so they follow the language of the current request instead of rendering the system language. Affected components: `SwContactForm` and `SwNewsletterForm` (salutation `displayName`), `SwStockInfo` (delivery time `name`), `SwVariantConfigurator` (property group `name` — the option values below it already used `translated`), `CmsElementBuyBox` (unit `name`), `CmsElementCrossSelling` (cross selling `name`), `CmsElementProductDescriptionReviews` (category `name`), `SwProductCardImage` and `CmsElementImageGallery` (media `alt`).
538
+
539
+ All of them now go through `getTranslatedProperty()` from `@shopware/helpers`, which falls back to the root property when `translated` is missing — so the rendered value only ever changes on storefronts whose language differs from the system language.
540
+
541
+ - [#2606](https://github.com/shopware/frontends/pull/2606) [`1c9a604`](https://github.com/shopware/frontends/commit/1c9a604e05c0fa1b27708a5338cd5690a0ff5c67) Thanks [@mkucmus](https://github.com/mkucmus)! - Stop leaking internal types into the layer's public type surface.
542
+
543
+ - `index.d.ts` now only augments the app config. It no longer re-exports `@shopware/composables` and `./.nuxt/imports`.
544
+ - The `#imports` shim moved to a private `types/imports.d.ts`, which is not published.
545
+ - `app.config` exports a plain object with `satisfies AppConfigInput` instead of calling `defineAppConfig`. No runtime change.
546
+
547
+ This fixes `Cannot find name 'ref'` and similar errors in the shared and node contexts when a project uses the Nuxt 4 project-references `tsconfig.json`.
548
+
549
+ - [#2601](https://github.com/shopware/frontends/pull/2601) [`e64d2f9`](https://github.com/shopware/frontends/commit/e64d2f9c13a99054d7e56e4b7ec2f1f23f9768b7) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Typography for CMS-authored HTML is now scoped to a `cms-element-text` class and ships from the layer as `app/assets/css/rich-text.css`, with overflow guards for long strings, media, `pre` and tables. Headings and lists in your own markup that had no explicit size or `list-*` class were relying on the removed global rules and will change.
550
+
551
+ - [#2663](https://github.com/shopware/frontends/pull/2663) [`7020545`](https://github.com/shopware/frontends/commit/70205458cb9357a068029d0aaef41898ab94b354) Thanks [@mkucmus](https://github.com/mkucmus)! - A linked CMS image always has an accessible name now. `CmsElementImage` names the link with the element's `ariaLabel`, then the media title, then a generic fallback translatable through `cms.image.linkWithoutLabel`. A decorative image skips the media title.
552
+
553
+ - Updated dependencies [[`2ddf156`](https://github.com/shopware/frontends/commit/2ddf156805b2941fe2069e78453fb3c4eb6d44ac), [`204c8f4`](https://github.com/shopware/frontends/commit/204c8f45f737e724db6d00b80c5faef8ddb77cb4), [`7020545`](https://github.com/shopware/frontends/commit/70205458cb9357a068029d0aaef41898ab94b354), [`183c183`](https://github.com/shopware/frontends/commit/183c183f905486c27fa770fd0f4cd9993e86c20e), [`458494e`](https://github.com/shopware/frontends/commit/458494e8bd2be88d4fbf161636a109c8f4efc443), [`183c183`](https://github.com/shopware/frontends/commit/183c183f905486c27fa770fd0f4cd9993e86c20e), [`183c183`](https://github.com/shopware/frontends/commit/183c183f905486c27fa770fd0f4cd9993e86c20e), [`8913956`](https://github.com/shopware/frontends/commit/89139563924163e57cafdd9770fe603f2dbd8cba), [`458494e`](https://github.com/shopware/frontends/commit/458494e8bd2be88d4fbf161636a109c8f4efc443)]:
554
+ - @shopware/helpers@1.8.0
555
+ - @shopware/composables@1.13.0
556
+ - @shopware/api-client@1.6.0
package/app/app.config.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { defineAppConfig } from "#imports";
1
+ import type { AppConfigInput } from "nuxt/schema";
2
2
 
3
- export default defineAppConfig({
3
+ export default {
4
4
  imagePlaceholder: {
5
5
  color: "#543B95",
6
6
  },
@@ -16,4 +16,4 @@ export default defineAppConfig({
16
16
  default: "(max-width: 768px) 50vw, 25vw",
17
17
  },
18
18
  unocssRuntime: true,
19
- });
19
+ } satisfies AppConfigInput;
@@ -0,0 +1,80 @@
1
+ .cms-element-text {
2
+ word-break: break-word;
3
+
4
+ :where(h1, h2, h3, h4, h5, h6) {
5
+ margin-bottom: var(--rte-heading-mb, 10px);
6
+ font-weight: var(--rte-heading-weight, 600);
7
+ }
8
+
9
+ :where(h1) {
10
+ font-size: var(--rte-h1-size, 2.25rem);
11
+ line-height: var(--rte-h1-line, 2.5rem);
12
+ }
13
+
14
+ :where(h2) {
15
+ font-size: var(--rte-h2-size, 1.75rem);
16
+ line-height: var(--rte-h2-line, 2rem);
17
+ }
18
+
19
+ :where(h3) {
20
+ font-size: var(--rte-h3-size, 1.25rem);
21
+ line-height: var(--rte-h3-line, 1.5rem);
22
+ }
23
+
24
+ :where(h4, h5, h6) {
25
+ font-size: var(--rte-h4-size, 1.125rem);
26
+ line-height: var(--rte-h4-line, 1.5rem);
27
+ }
28
+
29
+ :where(p, blockquote, pre) {
30
+ margin-bottom: var(--rte-flow, 1rem);
31
+ }
32
+
33
+ :where(ol, ul, dl) {
34
+ list-style-type: disc;
35
+ padding-left: var(--rte-list-indent, 40px);
36
+ margin-top: 0;
37
+ margin-bottom: var(--rte-flow, 1rem);
38
+ }
39
+
40
+ :where(ol) {
41
+ list-style-type: decimal;
42
+ }
43
+
44
+ :where(img, iframe, video) {
45
+ max-width: 100%;
46
+ }
47
+
48
+ :where(img) {
49
+ height: auto;
50
+ }
51
+
52
+ :where(table) {
53
+ width: 100%;
54
+ margin-bottom: var(--rte-flow, 1rem);
55
+ border-collapse: collapse;
56
+ }
57
+
58
+ :where(th, td) {
59
+ padding: var(--rte-cell-padding, 0.5rem 0.75rem);
60
+ border: 1px solid var(--rte-border-color, #e5e7eb);
61
+ }
62
+
63
+ :where(th, thead td) {
64
+ text-align: left;
65
+ font-weight: var(--rte-heading-weight, 600);
66
+ }
67
+
68
+ :where(hr) {
69
+ margin: var(--rte-flow, 1rem) 0;
70
+ border-top: 1px solid var(--rte-border-color, #e5e7eb);
71
+ }
72
+
73
+ > :last-child {
74
+ margin-bottom: 0;
75
+ }
76
+
77
+ :where(.sw-text-editor-table__col-selector) {
78
+ display: none;
79
+ }
80
+ }
@@ -3,6 +3,7 @@ import { ApiClientError } from "@shopware/api-client";
3
3
  import type { ApiError } from "@shopware/api-client";
4
4
  import type { CmsElementForm } from "@shopware/composables";
5
5
  import { useCmsTranslations } from "@shopware/composables";
6
+ import { getTranslatedProperty } from "@shopware/helpers";
6
7
  import { useVuelidate } from "@vuelidate/core";
7
8
  import { email, minLength, required } from "@vuelidate/validators";
8
9
  import { defu } from "defu";
@@ -189,7 +190,7 @@ const invokeSubmit = async () => {
189
190
  :key="salutation.id"
190
191
  :value="salutation.id"
191
192
  >
192
- {{ salutation.displayName }}
193
+ {{ getTranslatedProperty(salutation, "displayName") }}
193
194
  </option>
194
195
  </select>
195
196
  </div>
@@ -6,6 +6,9 @@ import type { Schemas } from "#shopware";
6
6
  type FilterState = {
7
7
  manufacturer: Set<string>;
8
8
  properties: Set<string>;
9
+ // Optional: consumers building their own filter-state object predate this
10
+ // field, so a missing one must render no category chips, not throw.
11
+ categories?: Set<string>;
9
12
  "min-price": number | undefined;
10
13
  "max-price": number | undefined;
11
14
  rating: number | undefined;
@@ -70,13 +73,14 @@ const activeChips = computed(() => {
70
73
  }
71
74
 
72
75
  // Add manufacturer filters
73
- const manufacturers = Array.from(props.filters.manufacturer);
74
- for (const manufacturerId of manufacturers) {
75
- const filter = props.availableFilters.find(
76
- (f) => f.code === "manufacturer",
77
- );
78
- if (filter && "entities" in filter && filter.entities) {
79
- const entity = filter.entities.find((e) => e.id === manufacturerId);
76
+ const manufacturerFilter = props.availableFilters.find(
77
+ (f) => f.code === "manufacturer",
78
+ );
79
+ if (manufacturerFilter?.entities) {
80
+ for (const manufacturerId of props.filters.manufacturer) {
81
+ const entity = manufacturerFilter.entities.find(
82
+ (e) => e.id === manufacturerId,
83
+ );
80
84
  const name = getTranslatedName(entity);
81
85
  if (name) {
82
86
  chips.push({
@@ -88,6 +92,24 @@ const activeChips = computed(() => {
88
92
  }
89
93
  }
90
94
 
95
+ // Add category filters
96
+ const categoryFilter = props.availableFilters.find(
97
+ (f) => f.code === "categories",
98
+ );
99
+ if (categoryFilter?.entities) {
100
+ for (const categoryId of props.filters.categories ?? []) {
101
+ const entity = categoryFilter.entities.find((e) => e.id === categoryId);
102
+ const name = getTranslatedName(entity);
103
+ if (name) {
104
+ chips.push({
105
+ label: name,
106
+ code: "categories",
107
+ value: categoryId,
108
+ });
109
+ }
110
+ }
111
+ }
112
+
91
113
  // Add price filters
92
114
  if (props.filters["min-price"] || props.filters["max-price"]) {
93
115
  const min = props.filters["min-price"] || 0;
@@ -3,6 +3,7 @@ import { ApiClientError } from "@shopware/api-client";
3
3
  import type { ApiError } from "@shopware/api-client";
4
4
  import type { CmsElementForm } from "@shopware/composables";
5
5
  import { useCmsTranslations } from "@shopware/composables";
6
+ import { getTranslatedProperty } from "@shopware/helpers";
6
7
  import { useVuelidate } from "@vuelidate/core";
7
8
  import type { ValidationRuleWithoutParams } from "@vuelidate/core";
8
9
  import { email, required } from "@vuelidate/validators";
@@ -247,7 +248,7 @@ const invokeSubmit = async () => {
247
248
  :key="salutation.id"
248
249
  :value="salutation.id"
249
250
  >
250
- {{ salutation.displayName }}
251
+ {{ getTranslatedProperty(salutation, "displayName") }}
251
252
  </option>
252
253
  </select>
253
254
  </div>
@@ -17,6 +17,7 @@ import {
17
17
  useAddToCart,
18
18
  useCartErrorParamsResolver,
19
19
  useCartNotification,
20
+ useNavigationContext,
20
21
  useNotifications,
21
22
  useProductWishlist,
22
23
  useUrlResolver,
@@ -144,10 +145,25 @@ const productManufacturer = computed(() =>
144
145
  getProductManufacturerName(product.value),
145
146
  );
146
147
 
147
- const { getUrlPrefix } = useUrlResolver();
148
- const productLink = computed(() =>
149
- buildUrlPrefix(getProductRoute(product.value), getUrlPrefix()),
148
+ const { foreignKey: currentNavigationId, routeName } = useNavigationContext();
149
+ const referrerCategoryId = computed(() =>
150
+ isProductListing && routeName.value === "frontend.navigation.page"
151
+ ? currentNavigationId.value
152
+ : undefined,
150
153
  );
154
+ const { getUrlPrefix } = useUrlResolver();
155
+ const productLink = computed(() => {
156
+ const productRoute = getProductRoute(product.value);
157
+ const productRouteWithReferrer = {
158
+ ...productRoute,
159
+ state: {
160
+ ...productRoute.state,
161
+ referrerCategoryId: referrerCategoryId.value,
162
+ },
163
+ };
164
+
165
+ return buildUrlPrefix(productRouteWithReferrer, getUrlPrefix());
166
+ });
151
167
  </script>
152
168
 
153
169
  <template>
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  type UrlRouteOutput,
4
4
  getSmallestThumbnailUrl,
5
+ getTranslatedProperty,
5
6
  isProductOnSale,
6
7
  isProductTopSeller,
7
8
  } from "@shopware/helpers";
@@ -37,7 +38,10 @@ const coverSrcPath = computed(() => {
37
38
  });
38
39
 
39
40
  const coverAlt = computed(() => {
40
- return props.product?.cover?.media?.alt || props.product?.translated?.name;
41
+ return (
42
+ getTranslatedProperty(props.product?.cover?.media, "alt") ||
43
+ props.product?.translated?.name
44
+ );
41
45
  });
42
46
 
43
47
  const isOnSale = computed(() => isProductOnSale(props.product));
@@ -2,6 +2,7 @@
2
2
  import { computed } from "vue";
3
3
  import type { Component } from "vue";
4
4
 
5
+ import SwFilterCategoriesVue from "./listing-filters/SwFilterCategories.vue";
5
6
  import SwFilterPriceVue from "./listing-filters/SwFilterPrice.vue";
6
7
  import SwFilterPropertiesVue from "./listing-filters/SwFilterProperties.vue";
7
8
  import SwFilterRatingVue from "./listing-filters/SwFilterRating.vue";
@@ -11,6 +12,7 @@ const {
11
12
  filter,
12
13
  selectedManufacturer,
13
14
  selectedProperties,
15
+ selectedCategories = new Set<string>(),
14
16
  selectedMinPrice,
15
17
  selectedMaxPrice,
16
18
  selectedRating,
@@ -20,6 +22,7 @@ const {
20
22
  filter: ListingFilter;
21
23
  selectedManufacturer: Set<string>;
22
24
  selectedProperties: Set<string>;
25
+ selectedCategories?: Set<string>;
23
26
  selectedMinPrice: number | undefined;
24
27
  selectedMaxPrice: number | undefined;
25
28
  selectedRating: number | undefined;
@@ -40,11 +43,13 @@ const transformedFilters = computed(() => ({
40
43
  "shipping-free": selectedShippingFree,
41
44
  manufacturer: [...selectedManufacturer],
42
45
  properties: [...selectedProperties],
46
+ categories: [...selectedCategories],
43
47
  }));
44
48
 
45
49
  const filterComponent = computed<Component | undefined>(() => {
46
50
  const componentMap: Record<string, Component> = {
47
51
  manufacturer: SwFilterPropertiesVue,
52
+ categories: SwFilterCategoriesVue,
48
53
  price: SwFilterPriceVue,
49
54
  rating: SwFilterRatingVue,
50
55
  "shipping-free": SwFilterShippingFreeVue,