Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions packages/cms-base-layer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,152 @@ export default mergeConfigs([config, {

See the [UnoCSS reference](https://unocss.dev/integrations/nuxt#configuration) for more information on how to configure UnoCSS in Nuxt when work with layers.

## 🖼️ Image Optimization

This layer includes [Nuxt Image](https://image.nuxt.com/) configuration optimized for Shopware 6 instances, with a custom provider that maps Nuxt Image modifiers to Shopware's query parameters (`width`, `height`, `quality`, `format`, `fit`).

> **Note for Cloud (SaaS) Users:** Image optimization and all modifiers used in the Nuxt Image module are handled automatically by Shopware Cloud infrastructure powered by [Fastly CDN](https://developer.shopware.com/docs/products/paas/shopware/cdn/). No additional configuration or plugins are required - simply use `<NuxtImg>` and all transformations (format conversion, quality adjustment, responsive sizing) work out of the box through Fastly's Image Optimizer.

### Features

- ✅ Automatic WebP/AVIF format conversion
- ✅ Responsive image sizing based on viewport
- ✅ Lazy loading support
- ✅ Quality optimization
- ✅ Multiple image presets for common use cases
- ✅ Works with Shopware Cloud (SaaS) and self-hosted instances

### Configuration

The layer comes pre-configured with optimized settings. No additional setup is required! The configuration includes:

**Available Presets:**
- `productCard` - Product listing images (WebP, quality 85, cover fit)
- `productDetail` - Product detail page images (WebP, quality 90, contain fit)
- `thumbnail` - Small thumbnails (150x150, WebP, quality 75)
- `hero` - Hero banners (WebP, quality 90, cover fit)

**Responsive Breakpoints:**
- `xs: 320px`, `sm: 640px`, `md: 768px`, `lg: 1024px`, `xl: 1280px`, `xxl: 1536px`

### Usage in Components

Replace standard `<img>` tags with `<NuxtImg>` to enable automatic optimization:

```vue
<!-- Using presets -->
<NuxtImg
src="https://cdn.shopware.store/media/path/to/image.jpg"
preset="productCard"
:width="400"
alt="Product"
loading="lazy"
/>

<!-- Custom modifiers -->
<NuxtImg
src="https://cdn.shopware.store/media/path/to/image.jpg"
:width="800"
:height="600"
format="webp"
:quality="85"
fit="cover"
alt="Custom image"
/>

<!-- Using with dynamic Shopware media URLs -->
<NuxtImg
:src="product.cover.media.url"
preset="productDetail"
:width="800"
:alt="product.cover.media.alt"
/>
```

### Supported Modifiers

Shopware supports the following URL parameters for image transformation:

| Modifier | Description | Example | Support |
|----------|-------------|---------|---------|
| `width` | Image width in pixels | `400` | ✅ Always supported |
| `height` | Image height in pixels | `600` | ✅ Always supported |
| `quality` | Image quality (0-100) | `85` | ⚠️ Cloud/Plugin required* |
| `format` | Output format | `webp`, `avif`, `jpg`, `png` | ⚠️ Cloud/Plugin required* |
| `fit` | Resize behavior | `cover`, `contain`, `fill` | ⚠️ Cloud/Plugin required* |

*Advanced transformations (quality, format, fit) are available in:
- **Shopware Cloud (SaaS)**: Built-in support via managed infrastructure. For a complete list of supported image transformation parameters, see [Fastly Image Optimizer Query Parameters](https://www.fastly.com/documentation/reference/io/#query-parameters).
- **Self-hosted instances**: Require thumbnail processor plugins like [FroshPlatformThumbnailProcessor](https://github.com/FriendsOfShopware/FroshPlatformThumbnailProcessor) or third-party CDN integration

### How It Works

This layer includes a custom Shopware provider for Nuxt Image that maps modifiers to Shopware's query parameters:
- `width` modifier → `?width=400`
- `height` modifier → `?height=300`
- `quality` modifier → `?quality=85`
- `format` modifier → `?format=webp`
- `fit` modifier → `?fit=cover`

When you use `<NuxtImg>`, the custom provider automatically converts your component props into the correct URL format for Shopware. The images are then processed on-the-fly by Shopware Cloud (SaaS) infrastructure or your configured thumbnail processor.

#### 🔍 Understanding Image Processing in Shopware

**Built-in Thumbnail Generation:**
Shopware has native thumbnail generation (using GD2 or ImageMagick) that creates predefined sizes (400x400, 800x800, 1920x1920) during image upload. These thumbnails are generated once and stored on your server.

**Dynamic On-the-Fly Transformations:**
For dynamic image transformations via query parameters (like `?width=800&format=webp`), you need **remote thumbnail generation** configured:

- **Shopware Cloud (SaaS)**: ✅ Fully supported out-of-the-box via Fastly CDN - all query parameters work automatically
- **Self-hosted**: ⚠️ Requires additional setup:
- Install a plugin like [FroshPlatformThumbnailProcessor](https://github.com/FriendsOfShopware/FroshPlatformThumbnailProcessor) for on-the-fly processing, OR
- Configure external middleware (Thumbor, Sharp, imgproxy) via [remote thumbnail generation](https://developer.shopware.com/docs/guides/plugins/plugins/content/media/remote-thumbnail-generation.html)

**Without remote thumbnail generation configured**, query parameters will be ignored and only the predefined static thumbnails will be served.

> **💡 Recommendation**: If you're self-hosting Shopware and want to use dynamic image transformations with Nuxt Image modifiers, install the FroshPlatformThumbnailProcessor plugin first to enable on-the-fly processing.

### Customizing Configuration

You can extend or override the default settings in your project's `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
extends: ["@shopware/cms-base-layer"],

image: {
// Change default quality
quality: 85,

// Add/change formats
formats: ['avif', 'webp', 'jpg'],

// Override or add presets
presets: {
// Override existing preset
productCard: {
modifiers: {
format: 'avif',
quality: 80,
fit: 'cover',
}
},
// Add custom preset
categoryBanner: {
modifiers: {
format: 'webp',
quality: 90,
width: 1200,
height: 400,
fit: 'cover',
}
}
}
}
})
```

## 📘 Available components

The list of available blocks and elements is [here](https://frontends.shopware.com/packages/cms-base-layer.html#available-components).
Expand Down
7 changes: 5 additions & 2 deletions packages/cms-base-layer/app/components/SwProductCardImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ const isTopseller = computed(() => isProductTopSeller(props.product));
<template>
<div class="self-stretch aspect-square relative flex flex-col justify-start items-start overflow-hidden">
<RouterLink :to="productLink" class="self-stretch h-full relative overflow-hidden">
<img ref="imageElement" class="w-full h-full absolute top-0 left-0 object-cover"
:src="coverSrcPath" :alt="coverAlt" data-testid="product-box-img" />
<div ref="imageElement" class="w-full h-full absolute top-0 left-0">
<NuxtImg preset="productCard" loading="lazy"
class="w-full h-full"
:src="coverSrcPath" :alt="coverAlt" data-testid="product-box-img" />
</div>
</RouterLink>

<div v-if="isTopseller || isOnSale"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,14 @@ const selectValue = (id: string) => {
<template>
<div class="self-stretch flex flex-col justify-start items-start gap-4">
<div class="self-stretch flex flex-col justify-center items-center">
<div
<div
class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-between items-center gap-1 cursor-pointer"
@click="toggle"
role="button"
tabindex="0"
:aria-expanded="isFilterVisible"
:aria-controls="props.filter.code"
:aria-label="props.filter.label"
@keydown.enter="toggle"
@keydown.space.prevent="toggle"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ const rightContent = getSlotContent("right");
const centerContent = getSlotContent("center");
</script>
<template>
<div class="cms-block-image-bubble-row grid md:grid-cols-3 gap-10">
<div class="flex items-center justify-center">
<div class="cms-block-image-bubble-row flex flex-col sm:flex-row justify-start items-start gap-6 w-full">
<div class="w-full sm:flex-1 flex items-center justify-center">
<CmsGenericElement :content="leftContent" class="w-full" />
</div>
<div class="flex items-center justify-center">
<div class="w-full sm:flex-1 flex items-center justify-center">
<CmsGenericElement :content="centerContent" class="w-full" />
</div>
<div class="flex items-center justify-center">
<div class="w-full sm:flex-1 flex items-center justify-center">
<CmsGenericElement :content="rightContent" class="w-full" />
</div>
</div>
</template>
<style scoped>
.cms-block-image-bubble-row .cms-element-image {
.cms-block-image-bubble-row :deep(.cms-element-image) {
@apply object-cover max-w-xs overflow-hidden rounded-full aspect-square;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,28 @@ const centerLeftContent = getSlotContent("center-left");
const centerRightContent = getSlotContent("center-right");
</script>
<template>
<div class="cms-block-image-four-column grid md:grid-cols-4 gap-10">
<CmsGenericElement :content="leftContent" />
<CmsGenericElement :content="centerLeftContent" />
<CmsGenericElement :content="centerRightContent" />
<CmsGenericElement :content="rightContent" />
<div class="cms-block-image-four-column flex flex-col sm:flex-row sm:flex-wrap lg:flex-nowrap justify-start items-start gap-6 w-full">
<div class="w-full sm:w-[calc(50%-12px)] lg:flex-1">
<CmsGenericElement :content="leftContent" />
</div>
<div class="w-full sm:w-[calc(50%-12px)] lg:flex-1">
<CmsGenericElement :content="centerLeftContent" />
</div>
<div class="w-full sm:w-[calc(50%-12px)] lg:flex-1">
<CmsGenericElement :content="centerRightContent" />
</div>
<div class="w-full sm:w-[calc(50%-12px)] lg:flex-1">
<CmsGenericElement :content="rightContent" />
</div>
</div>
</template>

<style scoped>
.cms-block-image-four-column .cms-element-image {
@apply object-cover;
.cms-block-image-four-column :deep(.cms-element-image) {
@apply relative h-full w-full;
}

.cms-block-image-four-column :deep(.cms-element-image img) {
@apply w-full h-full object-cover;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,25 @@ const rightContent = getSlotContent("right");
const centerContent = getSlotContent("center");
</script>
<template>
<div class="cms-block-image-highlight-row grid md:grid-cols-3 gap-10">
<CmsGenericElement :content="leftContent" />
<CmsGenericElement :content="centerContent" />
<CmsGenericElement :content="rightContent" />
<div class="cms-block-image-highlight-row flex flex-col sm:flex-row justify-start items-start gap-6 w-full">
<div class="w-full sm:flex-1">
<CmsGenericElement :content="leftContent" />
</div>
<div class="w-full sm:flex-1">
<CmsGenericElement :content="centerContent" />
</div>
<div class="w-full sm:flex-1">
<CmsGenericElement :content="rightContent" />
</div>
</div>
</template>

<style scoped>
.cms-block-image-highlight-row .cms-element-image {
@apply border-[12px] border-white;
.cms-block-image-highlight-row :deep(.cms-element-image) {
@apply relative h-full w-full border-[12px] border-surface-surface;
}

.cms-block-image-highlight-row :deep(.cms-element-image img) {
@apply w-full h-full object-cover;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ const rightContent = getSlotContent("right");
</script>

<template>
<div class="grid md:grid-cols-2 gap-10">
<div class="grid gap-10">
<CmsGenericElement :content="leftTopContent" />
<CmsGenericElement :content="leftBottomContent" />
<div class="flex flex-col md:flex-row justify-start items-start gap-6 w-full">
<div class="w-full md:flex-1 flex flex-col justify-start items-start gap-6">
<div class="w-full">
<CmsGenericElement :content="leftTopContent" />
</div>
<div class="w-full">
<CmsGenericElement :content="leftBottomContent" />
</div>
</div>
<div class="w-full md:flex-1">
<CmsGenericElement :content="rightContent" />
</div>
<CmsGenericElement :content="rightContent" />
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ const leftContent = getSlotContent("left");
const rightContent = getSlotContent("right");
</script>
<template>
<div class="grid md:grid-cols-2 gap-10">
<CmsGenericElement :content="leftContent" />
<CmsGenericElement :content="rightContent" />
<div class="flex flex-col md:flex-row justify-start items-start gap-6 w-full">
<div class="w-full md:flex-1">
<CmsGenericElement :content="leftContent" />
</div>
<div class="w-full md:flex-1">
<CmsGenericElement :content="rightContent" />
</div>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,33 @@ const rightText = getSlotContent("right-text");
const rightImage = getSlotContent("right-image");
</script>
<template>
<div class="cms-block-image-text-bubble grid grid-cols-3 auto-cols-max">
<div class="cms-element-column p-4">
<div class="flex justify-center">
<div class="cms-block-image-text-bubble flex flex-col sm:flex-row justify-start items-start gap-6 w-full">
<div class="w-full sm:flex-1">
<div class="self-stretch flex justify-center">
<CmsGenericElement
:content="leftImage"
class="object-center rounded-full"
style="height: calc(100vw / 3 - 64px); width: calc(100vw / 3 - 64px)"
class="object-center rounded-full w-48 h-48 sm:w-56 sm:h-56 lg:w-64 lg:h-64"
/>
</div>
<CmsGenericElement :content="leftText" />
<CmsGenericElement :content="leftText" class="self-stretch" />
</div>
<div class="cms-element-column p-4">
<div class="flex justify-center">
<div class="w-full sm:flex-1">
<div class="self-stretch flex justify-center">
<CmsGenericElement
:content="centerImage"
class="object-center rounded-full"
style="height: calc(100vw / 3 - 64px); width: calc(100vw / 3 - 64px)"
class="object-center rounded-full w-48 h-48 sm:w-56 sm:h-56 lg:w-64 lg:h-64"
/>
</div>
<CmsGenericElement :content="centerText" />
<CmsGenericElement :content="centerText" class="self-stretch" />
</div>
<div class="cms-element-column p-4">
<div class="flex justify-center">
<div class="w-full sm:flex-1">
<div class="self-stretch flex justify-center">
<CmsGenericElement
:content="rightImage"
class="object-center rounded-full"
style="height: calc(100vw / 3 - 64px); width: calc(100vw / 3 - 64px)"
class="object-center rounded-full w-48 h-48 sm:w-56 sm:h-56 lg:w-64 lg:h-64"
/>
</div>
<CmsGenericElement :content="rightText" />
<CmsGenericElement :content="rightText" class="self-stretch" />
</div>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ const leftContent = getSlotContent("left");
const rightContent = getSlotContent("right");
</script>
<template>
<article class="md:grid md:grid-cols-2 gap-10">
<CmsGenericElement
:content="leftContent"
class="cms-block-image-text-cover__image"
/>
<CmsGenericElement
:content="rightContent"
class="cms-block-image-text-cover__text"
/>
<article class="flex flex-col md:flex-row justify-start items-start gap-6 w-full">
<div class="w-full md:flex-1">
<CmsGenericElement :content="leftContent" />
</div>
<div class="w-full md:flex-1">
<CmsGenericElement :content="rightContent" />
</div>
</article>
</template>
Loading
Loading