Skip to content
Merged
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
4 changes: 4 additions & 0 deletions apps/docs/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export const sidebar = [
text: "Customize Components",
link: "/getting-started/cms/customize-components.html",
},
{
text: "Implement Missing Component",
link: "/getting-started/cms/missing-component.html",
},
{
text: "Create Blocks",
link: "/getting-started/cms/create-blocks.html",
Expand Down
64 changes: 64 additions & 0 deletions apps/docs/src/framework/shopping-experiences.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,70 @@ export default defineNuxtConfig({
});
```

## CMS rendering workflow

Understanding how the API data flows into rendered components helps you know what's automatic and what requires your own implementation.

### Data → Component mapping

The Shopware API returns a nested CMS tree:

```
CmsPage
└── CmsSection (type e.g. "default", "sidebar")
└── CmsBlock (type e.g. "image-text", "product-slider")
└── CmsSlot (type e.g. "image", "text")
```

Each node has a `type` field. The `cms-base-layer` package resolves a Vue component for every node by converting the type to a PascalCase component name:

| API node | type value | resolved component |
|---|---|---|
| `cms_section` | `default` | `CmsSectionDefault` |
| `cms_block` | `image-text` | `CmsBlockImageText` |
| `cms_slot` | `image` | `CmsElementImage` |

This resolution is done by `resolveCmsComponent` from `@shopware/composables`.

### What is handled automatically

The `@shopware/cms-base-layer` package ships ready-made components for all **default** Shopware CMS blocks and elements. If your project uses this package, those render without any configuration.

### What requires your implementation

Any CMS block or element type that is **not** part of the default set — typically custom blocks created in the Shopware backend — will not have a matching component. In development mode you will see an outlined placeholder where the component is missing, with:

- the expected component name to create (e.g. `CmsElementMyCustomSlider.vue`)
- a link to the relevant docs
- a one-click "copy AI prompt" button pre-filled with the live API data for that element

In production the placeholder is invisible (renders nothing), so missing components fail silently.

### Implementing a missing component

When a CMS element or block has no matching Vue component, the dev-mode placeholder gives you everything you need to get started quickly.

**Using the placeholder:**

1. Open your browser on any page that contains the unimplemented CMS type.
2. You will see a highlighted placeholder showing the expected component name (e.g. `CmsElementMyCustomSlider`).
3. Click **docs ↗** to open the relevant implementation guide.
4. Click **copy AI prompt** to copy a ready-to-use prompt to your clipboard. It includes:
- the exact component name and CMS type
- the full `content` prop JSON as returned by the API (including `config` and `data` fields)
- a reference to the docs
5. Paste the prompt into your AI coding assistant to generate a working first draft of the component.

**Completing the implementation:**

1. Create a file with the expected component name in your project, e.g. `components/CmsElementMyCustomSlider.vue`.
2. Nuxt auto-imports it as a global component, which is picked up by the resolver — the placeholder will disappear on the next hot-reload.
3. Use `defineProps<{ content: Schemas["CmsSlot"] }>()` and access `content.data` / `content.config` for the element's data.

::: tip Console warnings
In development mode the browser console also logs a warning for each missing component, including the component name to create and a direct link to the relevant docs page.
:::

## 3D / spatial media support

Shopping Experiences also support 3D models (GLB format) in image elements, image galleries, and the Spatial Viewer block. The 3D viewer is loaded on demand to keep the default bundle small. See [Working with Images — 3D and spatial media](../getting-started/page-elements/images.html#_3d-and-spatial-media-glb) for setup instructions.
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/getting-started/cms/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ nav:
Everything related to CMS ([Shopping Experiences](../../framework/shopping-experiences.html)).
<PageRef page="content-pages.html" title="Create content pages" sub="In this chapter you will learn how to display content pages with data from Shopware's own CMS." />
<PageRef page="customize-components.html" title="Customize Components" sub="In order to customize a component, you need to override it." />
<PageRef page="missing-component.html" title="Implement a Missing Component" sub="Step-by-step guide for when a CMS element or block has no matching Vue component." />
<PageRef page="create-blocks.html" title="Create Blocks" sub="In this chapter you will learn how to create CMS blocks." />
<PageRef page="create-elements.html" title="Create Elements" sub="In this chapter you will learn how to create CMS elements." />
<PageRef page="overwriting-cms.html" title="Overwrite CMS blocks in Nuxt 3 APP" sub="Example how to overwrite the product card." />
Expand Down
198 changes: 198 additions & 0 deletions apps/docs/src/getting-started/cms/missing-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
---
head:
- - meta
- name: og:title
content: Implement a Missing CMS Component
- - meta
- name: og:description
content: "Step-by-step guide to implementing a CMS element or block that is missing from your Shopware Frontends project."
- - meta
- name: og:image
content: "https://frontends-og-image.vercel.app/Missing%20CMS%20Component.png?fontSize=120px"
nav:
position: 25
---

<script setup>
import { useRoute } from 'vitepress'
import { computed, ref, watch, onMounted } from 'vue'

const route = useRoute()

const rawSearch = ref('')
onMounted(() => { rawSearch.value = window.location.search })
watch(() => route.path, () => { rawSearch.value = window.location.search })

const params = computed(() => {
const sp = new URLSearchParams(rawSearch.value)
return {
component: sp.get('component'),
type: sp.get('type'),
}
})

// Sanitize: only allow valid PascalCase component names (letters, numbers)
const componentName = computed(() => {
const raw = params.value.component || ''
const sanitized = raw.replace(/[^a-zA-Z0-9]/g, '')
return sanitized && /^Cms[A-Z]/.test(sanitized) ? sanitized : 'CmsElementMyCustomSlider'
})
const cmsType = computed(() => {
const allowed = ['element', 'block', 'section']
return allowed.includes(params.value.type || '') ? params.value.type : 'element'
})
const hasContext = computed(() => !!params.value.component)

const schemaType = computed(() => {
switch (cmsType.value) {
case 'block': return 'CmsBlock'
case 'section': return 'CmsSection'
default: return 'CmsSlot'
}
})
</script>

# Implement a Missing CMS Component

<div v-if="hasContext" class="custom-block tip">
<p class="custom-block-title">Your missing component</p>
<p>You need to create <strong>{{ componentName }}.vue</strong> ({{ cmsType }})</p>
</div>

You are here because a CMS {{ cmsType }} in your storefront has no matching Vue component. In development mode this shows as a highlighted placeholder instead of the actual content.

This page will take you from placeholder to working component in a few minutes.

## What is happening

The Shopware API returns a CMS tree of sections, blocks, and slots. Each node has a `type` field. The `cms-base-layer` package resolves a Vue component for each type by converting the name to PascalCase:

| API node | `type` value | expected component |
|---|---|---|
| `cms_section` | `sidebar` | `CmsSectionSidebar.vue` |
| `cms_block` | `image-text` | `CmsBlockImageText.vue` |
| `cms_slot` | `my-custom-slider` | `CmsElementMyCustomSlider.vue` |

If no matching component exists, the placeholder appears. Your job is to create that component.

## Is this a default Shopware CMS component?

The `@shopware/cms-base-layer` package ships implementations for all **default** Shopware 6 CMS blocks and elements. If you are seeing a placeholder for a type that ships with a standard Shopware 6 installation (not a custom plugin or your own block), this is a missing implementation in the package itself.

::: warning Missing a default component?
If the component type is part of **core Shopware 6 CMS** and is not covered by `cms-base-layer`, please open an issue so we can add it:

👉 [Create an issue on GitHub](https://github.com/shopware/frontends/issues/new?labels=cms-base)

Add the **`cms-base`** label to the issue. Include the component name shown in the placeholder, the `type` value, and the `apiAlias` from the API response. You can copy the full content JSON from the **copy AI prompt** button in the placeholder.
:::

If the component belongs to a custom plugin or you created the block yourself in the Shopware backend, continue with the steps below.

## Step 1 — Create the file

<div v-if="hasContext" class="custom-block tip">
<p class="custom-block-title">Your component</p>
<p>Create <code>components/{{ componentName }}.vue</code></p>
</div>

Create the file anywhere inside your `components/` directory. Nuxt picks it up automatically as a global component:

```
your-project/
└── components/
└── {{ componentName }}.vue ← create this
```

## Step 2 — Define the props

Every CMS component receives a single `content` prop. Use the Shopware schema type matching the CMS node type:

```vue
<!-- components/{{ componentName }}.vue -->
<script setup lang="ts">
import type { Schemas } from "#shopware";

const props = defineProps<{
content: Schemas["{{ schemaType }}"];
}>();
</script>
```

## Step 3 — Render the content

The `content` prop contains everything the API returned for that node. The exact fields depend on your CMS configuration in Shopware, but the structure is always:

- **`content.config`** — editor-configured settings (alignment, display mode, etc.)
- **`content.data`** — resolved data (media objects, products, etc.)
- **`content.translated`** — translated field values

Use the **copy AI prompt** button on the placeholder to get a pre-filled prompt that includes the full `content` JSON for your specific {{ cmsType }} — paste it into any AI assistant to generate a working first draft.

A minimal working {{ cmsType }}:

<div v-if="cmsType === 'block'">

```vue
<!-- components/{{ componentName }}.vue -->
<script setup lang="ts">
import type { Schemas } from "#shopware";

const props = defineProps<{
content: Schemas["CmsBlock"];
}>();

const { getSlotContent } = useCmsBlock(props.content);
const mainContent = getSlotContent("main");
</script>

<template>
<div>
<CmsGenericElement :content="mainContent" />
</div>
</template>
```

</div>

<div v-else>

```vue
<!-- components/{{ componentName }}.vue -->
<script setup lang="ts">
import type { Schemas } from "#shopware";

const props = defineProps<{
content: Schemas["CmsSlot"];
}>();

// config values are typed as `unknown` — assert the shape you need
const title = props.content.config?.title?.value as string | undefined;
</script>

<template>
<div>
<h2 v-if="title">{{ title }}</h2>
<!-- render content.data here -->
</div>
</template>
```

</div>

## Step 4 — Verify

Save the file. Vite will hot-reload and the placeholder will be replaced by your component. If it still shows, check that:

- the filename exactly matches the expected component name (PascalCase, `.vue` extension)
- the file is inside a directory that Nuxt scans for components

::: tip No restart needed
Nuxt's component auto-import picks up new files without restarting the dev server.
:::

## Going deeper

<PageRef page="create-elements.html" title="Create Elements" sub="Typed composables and helpers for working with CMS element data." />
<PageRef page="create-blocks.html" title="Create Blocks" sub="How to build block layouts with named slots." />
<PageRef page="customize-components.html" title="Customize existing components" sub="Override a default component from cms-base-layer." />
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
getBackgroundImageUrl,
getCmsLayoutConfiguration,
} from "@shopware/helpers";
import { h, provide } from "vue";
import { h, provide, resolveComponent } from "vue";
import { useAppConfig } from "#imports";
import type { Schemas } from "#shopware";
import { getImageSizes } from "../../../helpers/cms/getImageSizes";
Expand Down Expand Up @@ -67,7 +67,12 @@ const DynamicRender = () => {
}),
);
}
console.error(`Component not resolve: ${componentNameToResolve}`);
if (import.meta.dev) {
console.warn(
`[CMS] Block type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://frontends.shopware.com/getting-started/cms/create-blocks`,
);
return h(resolveComponent("CmsNoComponent"), { content: props.content });
}
return h("div", {}, "");
};
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { resolveCmsComponent } from "@shopware/composables";
import { getCmsLayoutConfiguration } from "@shopware/helpers";
import { h } from "vue";
import { h, resolveComponent } from "vue";
import type { Schemas } from "#shopware";

const props = defineProps<{
Expand All @@ -28,7 +28,12 @@ const DynamicRender = () => {
class: cssClasses,
});
}
console.error(`Component not resolved: ${componentNameToResolve}`);
if (import.meta.dev) {
console.warn(
`[CMS] Element type "${componentName}" is not implemented.\n → Create a component named "${componentNameToResolve}.vue" to render it.\n 📖 Docs: https://frontends.shopware.com/getting-started/cms/create-elements`,
);
return h(resolveComponent("CmsNoComponent"), { content: props.content });
}
return h("div", {}, "");
};
</script>
Expand Down
Loading
Loading