Implement a Missing CMS Component β
You are here because a CMS element 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 |
For a custom element, type is the name it was registered with in the Administration, so registerCmsElement({ name: "dailymotion" }) expects CmsElementDailymotion.vue.
If no matching block or element component exists, the placeholder appears in development, and the browser console logs a warning with the component name to create and a link to the docs. In production nothing renders, so a missing component fails silently. A missing section is the exception: CmsPage renders a plain "There is no β¦" line for it in every mode. Your job is to create the 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.
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
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. The backend side of a custom element is described in Add custom CMS element.
Step 1 β Create the file β
Create the file under your templateβs global CMS components dir (e.g. app/components/cms/), which templates register with global: true so resolveComponent can find it:
your-project/
βββ app/
βββ components/
βββ cms/
βββ {{ componentName }}.vue β create thisStep 2 β Define the props β
Every CMS component receives a single content prop. Use the Shopware schema type matching the CMS node type:
<!-- app/components/cms/{{ componentName }}.vue -->
<script setup lang="ts">
import type { Schemas } from "#shopware";
const props = defineProps<{
content: Schemas["CmsBlock"];
}>();
</script>For an element with its own settings, type config from the defaultConfig it was registered with. The backend guide registers its dailymotion element like this:
type CmsElementRegistration = {
name: string;
defaultConfig: {
dailyUrl: {
source: "static";
value: string;
};
};
};
declare const Shopware: {
Service(service: "cmsService"): {
registerCmsElement(config: CmsElementRegistration): void;
};
};
Shopware.Service("cmsService").registerCmsElement({
name: "dailymotion",
defaultConfig: {
dailyUrl: {
source: "static",
value: "",
},
},
});Each key of defaultConfig arrives in content.config as an ElementConfig holding source and value, which is how the element example in Step 3 types its config.
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 element β paste it into any AI assistant to generate a working first draft.
A minimal working element:
<!-- app/components/cms/{{ componentName }}.vue -->
<script setup lang="ts">
import type { ElementConfig } from "@shopware/composables";
import { computed, useCmsElementConfig } from "#imports";
import type { Schemas } from "#shopware";
type CmsElementMyCustomSlider = Omit<Schemas["CmsSlot"], "config"> & {
config: {
title?: ElementConfig<string>;
};
};
const props = defineProps<{
content: CmsElementMyCustomSlider;
}>();
const { getConfigValue } = useCmsElementConfig(props.content);
const title = computed(() => getConfigValue("title") || "");
</script>
<template>
<div>
<h2 v-if="title">{{ title }}</h2>
</div>
</template>Read the settings through getConfigValue rather than from content.config directly. It returns false for an entry whose source is mapped, because a mapped value comes from the surrounding entity, not from the element.
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,
.vueextension) - the file is inside a directory your
nuxt.config.tsregisters withglobal: trueβapp/components/cms/invue-starter-template. A file in plainapp/components/is auto-imported but not global, soresolveComponentdoes not find it.
No restart needed
Nuxt's component auto-import picks up new files without restarting the dev server.
Outside Nuxt nothing registers the file for you. Register the component globally, because resolveComponent only sees global components:
import { createApp } from "vue";
import CmsElementDailymotion from "./components/cms/CmsElementDailymotion.vue";
const app = createApp({});
app.component("CmsElementDailymotion", CmsElementDailymotion);