tenbruggencate / multibrand-lite
Shopware 6 multi-brand layer: serve up to 5 brand domains from one shop, hostname-resolved brand tokens injected into the page head.
Package info
bitbucket.org/Bruggencate/sw-plugin-multibrand-lite
Type:shopware-platform-plugin
pkg:composer/tenbruggencate/multibrand-lite
Requires
- php: >=8.1
- shopware/core: ~6.7
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
- shopware/storefront: ~6.7
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 2.2.0
- 2.1.4
- 2.1.3
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.0
- 1.5.0
- 1.4.1
- 1.4.0
- 1.3.1
- 1.3.0
- 1.2.2
- 1.2.1
- dev-feat/organization-structured-data
- dev-fix/w1-docs-truth
- dev-release/v2.1.3
- dev-fix/favicon-double-escape
- dev-docs/readme-more-from-links
- dev-fix/funnel-url-repoint
- dev-feat/suite-catalog-lite-pro-pairings
- dev-feat/rename-to-lite-package
- dev-feat/tb-suite-card
- dev-docs/cross-promotion
- dev-docs/lite-pro-strategy-resync-2
- dev-docs/handbook-sync
- dev-docs/lite-pro-strategy-resync
- dev-docs/readme-branding
- dev-chore/refresh-screenshots-v1.4.1
- dev-chore/audit-fixes-v1.4.0
- dev-chore/docs-screenshots-v1.3.1
- dev-chore/ux-polish-v1.3.0
- dev-chore/packagist-description-and-multi-sc-fix
- dev-chore/initial-extraction
This package is auto-updated.
Last update: 2026-09-05 11:54:05 UTC
README
Multi-Brand Lite
![]()
Hostname-driven brand layer for Shopware 6. One Shopware instance serves multiple brand domains; each request resolves to a brandKey in Twig and a set of --brand-* CSS custom properties in the page head.
License: MIT ยท Shopware: 6.7.x ยท PHP: 8.1 / 8.2
๐ฌ๐ง English ยท ๐ณ๐ฑ Nederlands ยท ๐ฉ๐ช Deutsch
Lite tier โ and what Pro adds
Multi-Brand Lite (this package, tenbruggencate/multibrand-lite) is the free tier of the Multi-Brand family. It handles the foundation: hostname-based brand resolution, per-brand theme variables (CSS custom properties), Twig helpers, and a five-slot brand config screen. If you just need "serve brand A from brand-a.tld and brand B from brand-b.tld with their own colours, fonts and logo", Lite is the whole answer.
Multi-Brand Pro (tenbruggencate/multibrand-pro) is released and installs side by side with Lite โ it requires Lite and extends it rather than replacing it. Pro is not published on Packagist: it is delivered directly today, and a Shopware Store listing is in preparation. What it adds:
- Per brand its own subject, sender name, HTML body and plain-text body for every transactional email type
- Applied automatically at send time on every path (Flow Builder or direct); the brand is derived from the sales channel's domain
- Override only what you want: empty fields fall back to the Shopware default; a toggle to pre-stage overrides before arming them
- Fail-safe: on any error the email goes out unchanged and the error is logged; Shopware's mail templates are never touched
- Managed under Settings โ Multi-Brand Pro with list and form, including hints for brand keys and template types; EN/NL/DE
- Export all data button: JSON with overrides and configuration, also via API; keep or remove data on uninstall
Lite alone is production-ready for the "shared catalogue, separate visual identity" use case and stays free.
Screenshots
Storefront homepage โ desktop, multibrand resolved per host |
Storefront homepage โ mobile (responsive) |
What it does
Serving two or three brand domains from a single Shopware install normally forces you to either (a) fork templates per brand or (b) duplicate the whole store as separate sales channels. Both paths are expensive to maintain. This plugin takes the middle path:
- One host โ one
brandKeyโ configure which hostnames resolve to which brand in the admin - Per-brand CSS tokens โ amethyst vs forest vs whatever you want, emitted as
var(--brand-primary)etc. so the same SCSS compiles once and renders differently per host - Twig helper โ
{{ brand() }}returns the current brand object; templates stay simple
No per-brand plugins, no duplicate theme compilation, no forked templates. Just a thin resolution layer and a token system.
Install
Multi-Brand Lite is free and distributable via Composer / Packagist or via the Shopware Store.
composer require tenbruggencate/multibrand-lite
bin/console plugin:refresh
bin/console plugin:install --activate TenBruggencateMultiBrand
bin/console cache:clear
The plugin class name stays TenBruggencateMultiBrand (grandfathered from v1.x โ only the composer package name changed in v2.0.0). If you're upgrading from v1.x see the v2.0.0 changelog entry for the one-line composer manifest update.
Configuration
Configured from Settings โ System โ Plugins โ Multi-Brand Lite โ Config. Brand definitions are read from the global config namespace and are deliberately not overridable per sales channel โ a brand is hostname-derived, so two sales channels on the same host share one brand by definition (see the design-decision note in BrandResolver). Shopware still renders its sales-channel selector on this screen; values saved against a specific sales channel are ignored at render time. The screen has a getting-started card, one card per brand slot โ Brand 1 (default) through Brand 5 โ and a JSON backup/restore card. Each slot N (1โ5) holds:
| Field | Purpose |
|---|---|
brandNKey | Machine key for the brand, e.g. north โ returned by brand_key() and rendered as data-brand |
brandNHostnamePattern | Hostname fragment that resolves to this brand (substring match, first match wins) |
brandNName, brandNEmail | Display name and contact address for templates and the Organization / WebSite JSON-LD |
brandNLogoPath, brandNFaviconBasePath | Asset paths for the brand logo and its favicon set |
brandNLegalName | Registered company name, emitted as schema.org legalName when it differs from the display name |
brandNStreetAddress, brandNPostalCode, brandNCity, brandNCountry | The PostalAddress block. brandNCountry must be an ISO 3166-1 alpha-2 code (two letters, normalised to upper case); anything else is dropped with a log warning |
brandNTelephone, brandNVatId | schema.org telephone and vatID |
brandNSameAs | Textarea, one absolute http(s) URL per line โ the brand's own social profiles and its Wikidata / Wikipedia entry. Lines that are not http(s) URLs are skipped, the rest are kept |
brandNPrimary, brandNSecondary, brandNSurface, brandNAccent1โ3, brandNTextStrong, brandNTextSoft | The eight colour tokens, emitted as --brand-* CSS custom properties |
The eight organisation fields (brandNLegalName through brandNSameAs) are optional and feed the per-brand Organization JSON-LD in meta.html.twig. Every one of them is omitted from the emitted data when empty, so a shop that never fills them in gets exactly the structured data it got before v2.2.0 โ plus the logo, which since v2.2.0 is emitted as an ImageObject. Logo fallback rule: the brand's own brandNLogoPath wins whenever it is set (made absolute against the request's scheme + host, or passed through untouched when it is already an absolute URL); the shared theme logo (theme_config('sw-logo-desktop')) is only used when the resolved brand has no logo path. On a multi-brand shop the theme logo is the same image for every brand, which is exactly the problem this rule solves.
Colour values must be 6-digit hex; anything else is replaced by a neutral default instead of being emitted into the page. Brand 1 is the fallback โ when no hostname pattern matches (admin preview URLs, direct IP access), brand 1 renders. Overlapping patterns between slots are logged.
Usage
In any Twig template:
<body data-brand="{{ brand_key() }}">
<h1>Welcome to {{ brand_config('name') }}</h1>
{% if is_brand('north') %}<p>Only on the north brand.</p>{% endif %}
</body>
brand_config() reads one field of the resolved brand: key, name, email, primary, secondary, surface, accent1โaccent3, textStrong, textSoft, logoPath, faviconBasePath, legalName, streetAddress, postalCode, city, country, telephone, vatId and sameAs (a list of validated URLs, every other field a string). brand_css_properties() returns the colour tokens as a --brand-* map; the routing config (hostnamePattern) is deliberately not exposed to templates.
meta.html.twig automatically emits the <style> block with --brand-* tokens; your SCSS reads them with graceful fallbacks:
.button--primary {
background: var(--brand-primary, #7C5BA5); // amethyst fallback
}
Standards
- Performance โ resolution happens once per request, cached per host. Token block adds ~400 bytes to
<head>; no runtime JS. - SEO โ no impact on URL structure, canonicals, or sitemaps. Each brand's sales channel retains its own SEO settings.
- WCAG โ CSS custom properties are a progressive enhancement; colour-contrast guarantees must be satisfied per-brand by the theme consuming the tokens (this plugin doesn't enforce them). Axe audit on a storefront page with the resolver active (0 violations on the plugin's own injection; theme-owned violations out of scope):
docs/ACCESSIBILITY.md. - GDPR โ stateless. No cookies, no tracking, no data stored per visitor. Full data-flow + subject-rights documentation in
GDPR.md. - Security โ host resolution uses
Request::getHost()(trusted-proxy aware); no user-controllable input touches the brand lookup. - Uninstall โ
plugin:uninstall --keep-user-datapreserves all brand config;plugin:uninstallwithout the flag drops everyTenBruggencateMultiBrand.config.*row so the destructive path leaves no trace. No owned tables.
Production-readiness checklist
A deliberately short list of things to verify before enabling the plugin on a customer-facing storefront. Not legal cover โ the MIT license already disclaims warranty โ but practical guidance an experienced operator would want anyway.
- [ ] Configure
trusted_proxiesif you sit behind a CDN / load balancer.Request::getHost()reads theHostheader (orX-Forwarded-Hostwhen proxies are trusted). Without trusted proxies set, the wrong hostname can resolve and visitors see the default brand instead of yours. - [ ] Set
defaultBrandto a valid existing brand key. This is the fallback for admin preview URLs and unmatched hostnames; pointing it at a non-existent key means the storefront renders with empty tokens and a brokendata-brandattribute. - [ ] Test each brand domain on staging. Hit
curl -I https://brand-a.staging.tld/and grep the response fordata-brand="brand-a"and the brand-specific--brand-primarytoken; if either is missing, host resolution isn't matching. - [ ] Don't try to use
hreflangbetween brand hostnames โ see the GEO note below for why. If your "brands" are actually locale-variants, use Shopware'sSalesChannelDomainper-language config instead and uninstall this plugin. - [ ] Theme contrast is per-brand, not per-plugin. This plugin injects tokens; your theme has to satisfy WCAG colour-contrast for every brand combination. Run an axe-core audit on each brand domain before launch.
Compatibility
Core platform
| Shopware | PHP | Status |
|---|---|---|
| 6.7.x โ tested against 6.7.8, 6.7.9 | 8.1, 8.2 | Stable |
| 6.6.x | โ | Not supported |
| 6.5.x and earlier | โ | Not supported |
Database
| Engine | Version | Notes |
|---|---|---|
| MySQL | 8.0+ | Primary target; JSON functions used for config-row manipulation in migrations |
| MariaDB | 10.11+ | Tested end-to-end; earlier versions lack some JSON operator support |
Browsers (storefront)
Evergreen browsers only โ the two most recent stable releases of each:
| Browser | Desktop | Mobile |
|---|---|---|
| Chrome / Chromium | โ | โ |
| Firefox | โ | โ |
| Safari | โ (macOS) | โ (iOS 16+) |
| Edge | โ | โ |
Internet Explorer and legacy Edge are not supported. The plugin emits no runtime JS, so graceful degradation on older browsers usually still renders content, just without progressive enhancements.
Admin browsers
Same evergreen matrix โ the Shopware admin is Vue-based and has its own compatibility baseline that this plugin doesn't extend or narrow.
Development
| Tool | Version | Scope |
|---|---|---|
| PHP | โฅ 8.1 | Runtime + test suite |
| Composer | 2.x | Dependency management |
| Node.js | โฅ 18 | Only needed if you edit SCSS and re-run the theme compile |
Accessibility
WCAG 2.2 level A + AA โ see docs/ACCESSIBILITY.md for axe-core audit output and per-page violations.
What we test before each release
- Full PHPUnit unit suite against PHP 8.1 + 8.2 (source-inspection tests don't need a kernel)
- PHPStan level 8 + PHP-CS-Fixer (@PSR12 + @Symfony)
- Composer validate
- Live-DB smoke tests (plugin install โ activate โ brand resolves per host โ uninstall cycle) against Shopware 6.7.8
Uninstall note
If you also install a theme plugin or another plugin that declares a composer dependency on tenbruggencate/multibrand-lite (or, for legacy installs, the pre-v2.0.0 name tenbruggencate/multibrand), Shopware's plugin manager will refuse to uninstall MultiBrand while that dependant is still active โ you'll see PluginHasActiveDependantsException. This is correct Shopware behaviour, not a MultiBrand bug: uninstalling MultiBrand first would break the dependant plugin at runtime.
To uninstall MultiBrand cleanly:
- Uninstall the dependant plugin(s) first, or remove the
requireentry from theircomposer.jsonand re-runcomposer update. - Then
bin/console plugin:uninstall TenBruggencateMultiBrand.
For vanilla MultiBrand installs (no composer-dependant plugin on top), uninstall runs unblocked: the plugin drops all TenBruggencateMultiBrand.config.* rows from system_config and removes itself.
GEO / multi-region note โ why there is no hreflang
This plugin deliberately does NOT emit hreflang alternate links between the configured brand hostnames. That is correct, not an oversight:
hreflangis the signal for "the same canonical content is served in multiple languages or regions" โ e.g.example.com/product(en) vs.example.de/product(de) for the same product.- The domains MultiBrand is designed for are separate brands with their own catalogues, copy, and identity, not locale-variants of the same storefront. They just share a single Shopware instance for operational reasons.
- Cross-linking them via
hreflangwould tell Google "these are equivalent pages, pick one" โ which is the opposite of what multi-brand storefronts want.
If your brands really ARE locale-variants (e.g. you have shop.nl / shop.de / shop.fr serving the same products in different languages), Shopware's native SalesChannelDomain per-language config is the right tool โ leave this plugin out of that use case.
Related plugins
Free Packagist-distributed siblings from the same publisher:
- TenBruggencateAnalytics โ multi-backend analytics (Matomo + Plausible) with optional per-brand custom dimensions
- TenBruggencateNewsletterLite โ GDPR-safe newsletter signup with double opt-in; stores the resolved brand with each subscriber
- TenBruggencateMaintenance โ branded maintenance page with HTTP 503 + Retry-After
- TenBruggencateLegalPages โ drop-in legal templates (privacy / terms / shipping / returns / disclaimer) with merge fields
Support
- Email: guy@tenbruggencatedevelopment.nl
- Docs & news: tenbruggencatedevelopment.nl
- Source / issues: Bitbucket repo
- Security vulnerabilities: see
SECURITY.mdโ email first, no public issues, 72-hour acknowledgement SLA
License
MIT ยฉ ten Bruggencate Development
More from ten Bruggencate Development
Focused, privacy-first Shopware 6 plugins โ free to start, upgrade when you grow:
- Newsletter โ Lite: GDPR-safe signup & subscriber-list management ยท Pro: campaigns, behavioural segmentation, product-pull blocks
- Legal Pages โ Lite: localized legal-page templates ยท Pro: compliance toolkit (cookie consent, accessibility statement, imprint validator, audit trail)
- Analytics โ Lite: multi-backend (Matomo / Plausible), GDPR-first ยท Pro: e-commerce conversion tracking
- Maintenance โ Lite: branded, SEO-correct (HTTP 503) maintenance page ยท Pro: scheduled maintenance windows
- Multi-Brand โ Lite: serve multiple brands from one Shopware, by hostname ยท Pro: per-brand transactional email overrides
- Product Encyclopedia โ Lite: structured educational content pages ยท Pro: per-profile related-product cross-links
- Seasons โ Lite: scheduled theme-config variants ยท Pro: per-variant marketing banners
Need more than a plugin? ten Bruggencate Development builds and customizes Shopware 6 stores. โ Let's talk