shopware/storefront
The Shopware Storefront package provides the customer-facing web frontend for Shopware 6, with Twig templates, theme assets, routing, and controllers for product, category, checkout, and account pages, enabling extensible responsive storefront experiences.
shopware/storefront is the Storefront bundle of Shopware 6 — the Twig/SCSS/JavaScript layer that renders the shop frontend. It is almost never installed standalone; it arrives as a dependency of shopware/platform (or the shopware/shopware project skeleton). The "67 stars / 0 dependents" figures are misleading: real usage is via the meta-package, and the code you'll actually touch is your own plugin's src/Resources/ tree, not this bundle's source.
Minimal setup:
composer create-project shopware/production my-shop
# or, for core hacking:
git clone https://github.com/shopware/shopware.git && cd shopware
composer install && ./bin/build-storefront.sh
Then, to see the layer you'll be extending, read these paths inside the bundle first:
Resources/views/storefront/ — base templates (layout/, page/, component/, block/, element/). This directory is the API you extend.Resources/app/storefront/src/plugin-system/ — the JS Plugin base class and PluginManager.Theme/ and Resources/theme.json — theme inheritance, SCSS variable pipeline.Page/ + Controller/ — the page loader that assembles PageletStruct objects into PageStruct for rendering.First use case, in order of likelihood: override one template block and add a JS plugin. Scaffold a plugin (bin/console plugin:create MyPlugin), then:
{# src/Resources/views/storefront/block/cms-block-image-text.html.twig #}
{% sw_extends '@Storefront/storefront/block/cms-block-image-text.html.twig' %}
{% block block_image_text_text %}
<div class="my-banner">{{ parent() }}</div>
{% endblock %}
// src/Resources/app/storefront/src/main.js
import MyPlugin from './my-plugin.plugin';
const PluginManager = window.PluginManager;
PluginManager.register('MyPlugin', MyPlugin, '[data-my-plugin]');
bin/console theme:compile
bin/console cache:clear
If the template renders but the JS doesn't, you skipped the asset build — see Gotchas.
Template extension is block-based, not file-based. Shopware compiles a template inheritance chain across all active plugins plus the theme. sw_extends is a Shopware tag, not {% extends %} — it records your template in the chain so other plugins can extend you. Always call {{ parent() }} unless you intend to replace. Override the narrowest block you can find rather than copying a whole file; block names are stable-ish, template paths are not.
Page loading: extend the loader, not the controller. Storefront controllers are thin; the work happens in AbstractPageLoader subclasses registered via the DI container tag shopware.storefront.page_loader. To add data to a product detail page:
class MyProductPageLoader extends AbstractPageLoader
{
public function load(Shopware\HttpFoundation\Request $request, SalesChannelContext $context): PageStruct
{
$page = $this->decorated->load($request, $context); // decorate, don't replace
if ($page instanceof ProductPage) {
$page->getProduct()->addExtension('my_extension', new ArrayStruct(['foo' => 'bar']));
}
return $page;
}
}
Register as a decorator on Shopware\Storefront\Page\Product\ProductPageLoader. Extensions surface in Twig as product.extensions.my_extension.foo. This is the cleanest integration point and survives Shopware minor upgrades far better than controller overrides.
JS plugins are declarative via data attributes. PluginManager.register('Name', Class, selector) instantiates the class per matching element, mapping data-name-options='{"foo":1}' to this.options.foo and data-name="true" to the selector. Options merge from defaultOptions in the class. Communicate via this.$emitter.publish('eventName', payload) and this.$emitter.subscribe(...) rather than reaching into other plugins' internals. Use this._registerInstance / PluginManager.initializePlugins() after injecting DOM via AJAX, otherwise dynamically added elements get no plugin.
Theme config is the SCSS API. Anything you want to make swappable per sales channel goes into theme.json:
{
"name": "MyTheme",
"author": "me",
"views": ["@Storefront", "@MyPlugin"],
"style": ["src/scss/overrides.scss"],
"config": {
"sw-color-brand-primary": {
"label": { "en-GB": "Primary", "de-DE": "Primär" },
"type": "color",
"value": "#0d6efd",
"scss": true
}
}
}
With "scss": true the value lands as $sw-color-brand-primary in the SCSS compile, so overrides.scss can reference it. views controls template fallback order — put @Storefront last so your theme wins.
Snippets, not hardcoded strings. src/Resources/snippet/storefront.en-GB.json under the storefront namespace; reference with {{ 'myPlugin.label'|trans }}. Missing snippet keys fail silently in production and print the raw key in dev.
Cache and HTTP layer are part of the design. Pagelets, ESI, and sw_ cache tags mean a naive {% if app.request... %} in a shared layout block can poison cached fragments for other sales channels. If output varies by context, put it behind a pagelet or an AbstractCartSubscriber-style event, and check HttpCacheKeyGenerator behavior when debugging "wrong user's cart" reports.
theme:compile compiles SCSS/JS into public/theme/. In dev use bin/watch-storefront.sh (or ./psh.phar storefront:watch in older setups). New JS files added to Resources/app/storefront/src/ require a rebuild — a cache:clear will not pick them up.sw_extends vs extends. Using plain Twig extends compiles fine and silently breaks the inheritance chain for every plugin downstream. Same for {% sw_include %} vs {% include %} when you want the include to be overridable.parent() in a block that has no parent throws. When you replace a block rather than wrap it, drop the call. When you extend a template but override a block defined in a grandparent, parent() still resolves — but debugging "block not found" errors almost always means a typo in a block name that was renamed between minor versions. Diff against the bundle source after upgrades.theme.json changes need theme:refresh semantics. New config fields don't appear in the admin until the theme is re-registered/compiled; a plain cache:clear leaves stale compiled CSS. If your new SCSS variable is undefined, it's a stale compile, not a PHP bug.data-foo-options='{"limit": 5}' gives you the number; data-foo-limit="5" gives you "5". Use the options attribute for anything non-boolean, and validate in init().'FormSubmit' means the second wins silently. Namespace your plugin names (MyVendorFormSubmit).PluginManager runs before your DOM is guaranteed. For plugins that manipulate layout, use init() for setup and subscribe to PluginEvents or DOMContentLoaded-adjacent hooks for reads that need final layout. Reading offsetWidth in init() is a classic zero-value bug.vendor/. Every change dies on composer update, and Shopware's own upgrade path assumes an unmodified vendor tree. Overrides belong in a plugin or a project theme.{{ dump() }} needs TwigBundle debug enabled; more reliably, bin/console debug:twig --filter=sw_extends and bin/console debug:container --tag=shopware.storefront.page_loader tell you what's actually registered. bin/console lint:twig src/Resources/views catches the syntax errors that only surface at render time otherwise.StorefrontRenderEvent (last-mile template params), ThemeCompiler events for asset pipelines, AbstractPageLoader decoration, StorefrontController route defaults (@Storefront annotation sets the route scope), and the storefront snippet namespace. Most "I need to change core behavior" problems have one of these as the intended answer.shopware/core and shopware/storefront to the same version constraint and read the UPGRADE notes for block renames before bumping.How can I help you explore Laravel packages today?