Documentation Index
Fetch the complete documentation index at: https://mintlify.com/shopware/meteor/llms.txt
Use this file to discover all available pages before exploring further.
This example demonstrates creating a complete custom CMS element with preview, configuration, and rendering.
Overview
We’ll create a video player element for Dailymotion that includes:
- Element registration
- Preview component
- Configuration component
- Element rendering component
Project Structure
src/
├── frontend/
│ ├── init/
│ │ └── init-app.ts
│ ├── cms/
│ │ └── ex-dailymotion/
│ │ ├── ex-dailymotion-constants.ts
│ │ ├── ex-dailymotion-preview.vue
│ │ ├── ex-dailymotion-config.vue
│ │ └── ex-dailymotion-element.vue
│ └── locations/
│ └── init-locations.ts
Complete Implementation
Step 1: Define Constants
cms/ex-dailymotion/ex-dailymotion-constants.ts
const CMS_DAILYMOTION_ELEMENT_NAME = 'ex-dailymotion';
export default {
CMS_DAILYMOTION_ELEMENT_NAME,
PUBLISHING_KEY: `${CMS_DAILYMOTION_ELEMENT_NAME}__config-element`,
};
Step 2: Register the CMS Element
import { cms } from '@shopware-ag/meteor-admin-sdk';
import EX_DAILYMOTION_CONSTANTS from '../cms/ex-dailymotion/ex-dailymotion-constants';
// Register CMS element
void cms.registerCmsElement({
name: EX_DAILYMOTION_CONSTANTS.CMS_DAILYMOTION_ELEMENT_NAME,
label: 'Dailymotion video',
defaultConfig: {
dailyUrl: {
source: 'static',
value: '',
},
},
});
// This creates three location IDs:
// - ex-dailymotion-element
// - ex-dailymotion-preview
// - ex-dailymotion-config
Step 3: Create Preview Component
cms/ex-dailymotion/ex-dailymotion-preview.vue
<template>
<div class="dailymotion-preview">
<div class="preview-icon">
<svg
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 5v14l11-7z"
fill="#0066DC"
/>
</svg>
</div>
<p class="preview-label">Dailymotion Video</p>
<p class="preview-description">
Add a Dailymotion video to your page
</p>
</div>
</template>
<style scoped>
.dailymotion-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
background: #f7fafc;
border: 2px dashed #cbd5e0;
border-radius: 8px;
min-height: 150px;
}
.preview-icon {
margin-bottom: 12px;
}
.preview-label {
font-weight: 600;
color: #2d3748;
margin-bottom: 4px;
}
.preview-description {
font-size: 12px;
color: #718096;
text-align: center;
}
</style>
Step 4: Create Configuration Component
cms/ex-dailymotion/ex-dailymotion-config.vue
<template>
<div class="dailymotion-config">
<div class="config-header">
<h3>Dailymotion Video Configuration</h3>
<p>Configure your Dailymotion video element</p>
</div>
<div class="config-form">
<sw-text-field
label="Dailymotion Video URL"
placeholder="https://www.dailymotion.com/video/..."
v-model="elementConfig.dailyUrl.value"
@input="onChange"
helpText="Enter the full Dailymotion video URL"
/>
<div v-if="videoId" class="preview-section">
<h4>Preview</h4>
<div class="video-preview">
<iframe
:src="embedUrl"
width="100%"
height="200"
frameborder="0"
allowfullscreen
/>
</div>
</div>
<div v-else class="no-preview">
<p>Enter a valid Dailymotion URL to see preview</p>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { data } from '@shopware-ag/meteor-admin-sdk';
import { SwTextField } from '@shopware-ag/meteor-component-library';
import EX_DAILYMOTION_CONSTANTS from './ex-dailymotion-constants';
export default defineComponent({
components: {
'sw-text-field': SwTextField,
},
data() {
return {
elementConfig: {
dailyUrl: {
source: 'static',
value: '',
},
},
};
},
computed: {
videoId() {
const url = this.elementConfig.dailyUrl.value;
if (!url) return null;
// Extract video ID from Dailymotion URL
const match = url.match(/dailymotion\.com\/video\/([a-zA-Z0-9]+)/);
return match ? match[1] : null;
},
embedUrl() {
return this.videoId
? `https://www.dailymotion.com/embed/video/${this.videoId}`
: '';
},
},
methods: {
onChange() {
// Publish config changes to element
data.update({
id: EX_DAILYMOTION_CONSTANTS.PUBLISHING_KEY,
data: {
config: this.elementConfig,
},
});
},
async loadConfig() {
try {
const result = await data.get({
id: EX_DAILYMOTION_CONSTANTS.PUBLISHING_KEY,
});
if (result?.config) {
this.elementConfig = result.config;
}
} catch (error) {
console.error('Failed to load config:', error);
}
},
},
mounted() {
this.loadConfig();
// Subscribe to external config changes
data.subscribe(
EX_DAILYMOTION_CONSTANTS.PUBLISHING_KEY,
(response) => {
if (response.data?.config) {
this.elementConfig = response.data.config;
}
}
);
},
});
</script>
<style scoped>
.dailymotion-config {
padding: 20px;
}
.config-header {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
}
.config-header h3 {
margin-bottom: 4px;
color: #2d3748;
}
.config-header p {
color: #718096;
font-size: 14px;
}
.config-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.preview-section {
padding: 16px;
background: #f7fafc;
border-radius: 8px;
}
.preview-section h4 {
margin-bottom: 12px;
color: #2d3748;
}
.video-preview {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
border-radius: 4px;
}
.video-preview iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.no-preview {
padding: 40px;
text-align: center;
background: #edf2f7;
border-radius: 8px;
color: #718096;
}
</style>
Step 5: Create Element Component
cms/ex-dailymotion/ex-dailymotion-element.vue
<template>
<div class="dailymotion-element">
<div v-if="videoId" class="video-container">
<iframe
:src="embedUrl"
width="100%"
height="100%"
frameborder="0"
allowfullscreen
allow="autoplay; fullscreen; picture-in-picture"
/>
</div>
<div v-else class="no-video">
<div class="placeholder-icon">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M8 5v14l11-7z"
fill="#cbd5e0"
/>
</svg>
</div>
<p>No video configured</p>
<p class="hint">Configure this element to add a Dailymotion video</p>
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { data } from '@shopware-ag/meteor-admin-sdk';
import EX_DAILYMOTION_CONSTANTS from './ex-dailymotion-constants';
export default defineComponent({
data() {
return {
videoUrl: '',
};
},
computed: {
videoId() {
if (!this.videoUrl) return null;
// Extract video ID from Dailymotion URL
const match = this.videoUrl.match(/dailymotion\.com\/video\/([a-zA-Z0-9]+)/);
return match ? match[1] : null;
},
embedUrl() {
return this.videoId
? `https://www.dailymotion.com/embed/video/${this.videoId}?autoplay=0`
: '';
},
},
methods: {
async loadConfig() {
try {
const result = await data.get({
id: EX_DAILYMOTION_CONSTANTS.PUBLISHING_KEY,
});
if (result?.config?.dailyUrl?.value) {
this.videoUrl = result.config.dailyUrl.value;
}
} catch (error) {
console.error('Failed to load element config:', error);
}
},
},
mounted() {
this.loadConfig();
// Subscribe to config changes for live preview
data.subscribe(
EX_DAILYMOTION_CONSTANTS.PUBLISHING_KEY,
(response) => {
if (response.data?.config?.dailyUrl?.value) {
this.videoUrl = response.data.config.dailyUrl.value;
}
}
);
},
});
</script>
<style scoped>
.dailymotion-element {
width: 100%;
}
.video-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
background: #000;
border-radius: 8px;
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.no-video {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
background: #f7fafc;
border: 2px dashed #cbd5e0;
border-radius: 8px;
min-height: 200px;
}
.placeholder-icon {
margin-bottom: 16px;
opacity: 0.5;
}
.no-video p {
color: #718096;
margin-bottom: 4px;
}
.no-video .hint {
font-size: 12px;
color: #a0aec0;
}
</style>
Step 6: Register Locations
locations/init-locations.ts
import { createApp, h, defineAsyncComponent } from 'vue';
import { location } from '@shopware-ag/meteor-admin-sdk';
import '@shopware-ag/meteor-component-library/styles.css';
const locations = {
'ex-dailymotion-config': defineAsyncComponent(
() => import('../cms/ex-dailymotion/ex-dailymotion-config.vue')
),
'ex-dailymotion-preview': defineAsyncComponent(
() => import('../cms/ex-dailymotion/ex-dailymotion-preview.vue')
),
'ex-dailymotion-element': defineAsyncComponent(
() => import('../cms/ex-dailymotion/ex-dailymotion-element.vue')
),
};
const app = createApp({
render: () => h(locations[location.get()])
});
app.mount('#app');
Usage in CMS
Navigate to Content > Shopping Experiences
Create new or edit existing layout
In the element sidebar, find “Dailymotion video”
Drag and drop into your layout
You’ll see the preview placeholder
Click the element to open settings
Click the configuration button
Enter a Dailymotion URL:
https://www.dailymotion.com/video/x8b2q3k
See live preview in the config modal
Save the configuration
The element updates with the video embed
Save the Shopping Experience
View on the storefront
Configuration Data Flow
- Config component updates data via
data.update()
- Publishing key broadcasts changes
- Element component receives updates via
data.subscribe()
Creating a CMS Block
You can also register a block that uses your element:
import { cms } from '@shopware-ag/meteor-admin-sdk';
await cms.registerCmsBlock({
name: 'ex-dailymotion-block',
label: 'Dailymotion Video Block',
category: 'video',
slots: [
{
element: 'ex-dailymotion',
}
],
slotLayout: {
grid: 'auto / auto'
},
});
Advanced: Two-Column Video Block
Create a block with two videos side by side:
await cms.registerCmsBlock({
name: 'ex-two-column-video',
label: 'Two Column Videos',
category: 'video',
slots: [
{
element: 'ex-dailymotion',
},
{
element: 'ex-dailymotion',
}
],
slotLayout: {
grid: 'auto / 1fr 1fr'
},
previewImage: 'https://example.com/preview.png'
});
Expected Output
In CMS Editor
- Element appears in sidebar with preview
- Drag-and-drop functionality works
- Configuration modal opens on click
- Live preview updates as you type
On Storefront
- Responsive video embed
- Full Dailymotion player functionality
- Proper aspect ratio maintained
Best Practices
- Use vendor prefix: Name elements uniquely (e.g.,
my-company-video)
- Provide defaults: Always set default config values
- Live preview: Subscribe to config changes in element component
- Error handling: Handle missing or invalid URLs gracefully
- Responsive design: Ensure elements work on all screen sizes
- Accessibility: Add proper ARIA labels and keyboard support
Common Issues
Config Not Updating
Ensure publishing key matches:
// Must be consistent across all components
const PUBLISHING_KEY = 'element-name__config-element';
Element Not Showing
Verify location IDs are registered:
// For element name: 'my-element'
// These must be registered:
'my-element-element'
'my-element-preview'
'my-element-config'
Learn More