Notes / February 27, 2026

Custom CMS blocks in Shopware (with a minimal code shape)

From admin configuration to storefront rendering—the pieces that must line up.

Shopware’s CMS lets merchandisers compose pages from slots and elements. A custom block (or element) is the right abstraction when marketing needs a repeatable pattern—hero + KPI row + product stream—without shipping PHP deploys for copy tweaks.

Below is the full shape I expect on projects: PHP data loading, Vue admin config, storefront render, and tests.


1. Terminology (do not mix these up)

  • CMS page — URL route or layout assignment.
  • Section / block — layout regions; your code may register a block containing one or more elements.
  • Element — the thing with a cms_slot config and a resolver.

Teams say “custom block” when they mean custom element—align language in README so new devs grep the right folders.


2. PHP: element definition + resolver

Registration (services.xml)

  • Tag your resolver: shopware.cms.data_resolver with entity attribute matching your element name.

Resolver (CmsElementResolverInterface)

  • getType(): string identifier, e.g. my-brand-highlight-grid.
  • collect(): add criteria for entities you need (products, media, categories)—Shopware batches these for performance.
  • enrich(): hydrate ElementDataCollection with real entities for the slot.

Performance

  • Never query inside loops over slots without batching—use collect aggregation patterns from core elements (product-slider, etc.) as reference.

Caching

  • If resolver hits external HTTP (rare—avoid), cache with TTL in resolver context; CMS pages are HTTP-cached aggressively.

3. Admin: Vue component

File naming: sw-cms-el-my-brand-highlight-grid (follow Shopware conventions).

Config fields

  • sw-entity-single-select for categoryId or productStreamId.
  • sw-media-upload for background.
  • sw-text-field for headline / legal footnote.

Validation

  • Use mapInheritance + cmsPageState patterns so layout assignments inherit sensibly.

Preview

  • Wire mixins.cms-element so the admin preview shows real products when possible.

4. Storefront: Twig (or JS storefront)

Twig element template (theme)

  • Path must match element name registered in theme’s theme.json element config.
  • Access hydrated data via element.data (shape depends on resolver).

Example Twig loop (illustrative)

{% if element.data.products %}
  <div class="cms-element-my-grid">
    {% for product in element.data.products %}
      {% sw_include '@Storefront/storefront/component/product/card/box.html.twig' with {
        layout: 'standard',
        displayMode: 'standard'
      } %}
    {% endfor %}
  </div>
{% endif %}

JS storefront (if using Storefront JS stack)

  • Register component mapping element name → Vue/Svelte component; lazy-load images.

5. Minimal PHP resolver sketch

public function collect(CmsSlotEntity $slot, ResolverContext $resolverContext): void
{
    $config = $slot->getFieldConfig();
    if (!$config || !$config->isProductStream()) {
        return;
    }
    $streamId = $config->getProductStream()->getValue();
    if (!$streamId) {
        return;
    }
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('streamId', $streamId));
    // Add association graph you need for cards (cover, prices) — keep tight.
    $slot->getData()->addProductStream('myStream', $streamId, $criteria);
}

(Exact APIs vary by Shopware minor version—compare to core ProductSliderCmsElementResolver.)


6. Testing strategy

  • Unit: resolver criteria correctness (stream id missing → no collect).
  • Integration: render CMS page via Storefront test router; assert HTML contains SKU you seeded.
  • Visual: Percy/Chromatic on admin preview if marketing cares about WYSIWYG fidelity.

7. Failure modes we have seen

  • Element works in default theme but not client child theme because theme.json did not register template path.
  • Cross-selling slots accidentally pull wrong currency because SalesChannelContext missing in preview resolver.

Resume framing

“Shipped Shopware CMS elements with resolver batching, Vue admin config, storefront templates, and automated coverage—reduced merchandising deploy churn.”