Skip to content

Twig-Overrides

Guppy verfolgt einen komponentenbasierten Templating-Ansatz: dynamische, modulare Templates, die per theme.json gesteuert werden und sich saubere von Shopwares Defaults trennen lassen. Ziel ist Updatefähigkeit ohne Patches im Kerncode.

Warum dieser Ansatz?

  1. Maximale Anpassbarkeit: Konfigurations-Werte statt Code-Änderungen.
  2. Modularität: Komponenten (Footer, Header, Widgets) als eigenständige Templates.
  3. Upgrade-Kompatibilität: Override via sw_extends, kein Eingriff im Kern.
  4. Barrierefreiheit: modulare Templates erlauben gezielte A11y-Anpassungen.

Theme-Integration via theme.json

Felder aus theme.json werden in Twig per theme_config('feld-name') gelesen. Beispiel: Footer-Variante.

twig
{# src/Resources/views/storefront/base.html.twig #}
{% if theme_config('guppy-footer') != "default" %}
    {% set configFooter = theme_config('guppy-footer') %}
{% endif %}

{% block base_footer %}
    <footer class="footer-main{% if configFooter %} footer-main-{{ configFooter }} pt-md-3 pt-4{% endif %}">
        {% block base_footer_inner %}
            {% if configFooter %}
                {% sw_include '@Storefront/storefront/layout/footer/footer-' ~ configFooter ~ '.html.twig' %}
            {% else %}
                {{ parent() }}
            {% endif %}
        {% endblock %}
    </footer>
{% endblock %}

Passendes theme.json-Feld:

json
{
  "guppy-footer": {
    "label": { "en-GB": "Footer variant", "de-DE": "Footer Variante" },
    "type": "text",
    "value": "guppy-default",
    "custom": {
      "componentName": "sw-single-select",
      "options": [
        { "value": "default",       "label": { "de-DE": "Shopware Standard" } },
        { "value": "guppy-default", "label": { "de-DE": "Guppy Standard" } }
      ]
    },
    "editable": true,
    "tab": "footer",
    "block": "layoutFooter",
    "order": 100
  }
}

Zugehöriges Variant-Template:

twig
{# src/Resources/views/storefront/layout/footer/footer-guppy-default.html.twig #}
{% sw_extends '@Storefront/storefront/layout/footer/footer.html.twig' %}

{# Hier Inhalt #}

Template-Varianten-System

Guppy implementiert das Varianten-Pattern durchgängig. Beispiele:

twig
{% if theme_config('guppy-header') != "default" %}
    {% set configHeader = theme_config('guppy-header') %}
{% endif %}

{% block base_header %}
    <header class="header-main{% if configHeader %} header-variant-{{ configHeader }}{% endif %}">
        {% block base_header_inner %}
            {% if configHeader %}
                {% sw_include '@Storefront/storefront/layout/header/header-' ~ configHeader ~ '.html.twig' %}
            {% else %}
                {{ parent() }}
            {% endif %}
        {% endblock %}
    </header>
{% endblock %}

Verfügbare Varianten: default, compact (Guppy Standard), extended (mit Top-Bar), simple.

twig
{% if theme_config('guppy-navigation') != "content" %}
    {% set navigationStyle = theme_config('guppy-navigation') %}
{% endif %}

{% block base_navigation %}
    <nav class="nav-main{% if navigationStyle %} nav-{{ navigationStyle }}{% endif %}">
        {% sw_include '@Storefront/storefront/layout/navbar/' ~ (navigationStyle ?: 'content') ~ '.html.twig' %}
    </nav>
{% endblock %}

Verfügbare Varianten: content, content-compact.

Productcards

twig
{% if theme_config('guppy-productcard-config') %}
    {% set boxDesign = theme_config('guppy-productcard-config') %}
{% endif %}

<article class="card product-box box-{{ layout }}{% if boxDesign %} box-{{ boxDesign }}{% endif %}">
    <!-- Produktkarten-Inhalt -->
</article>

Verfügbare Varianten: default, guppy-default (barrierefrei). Zusätzliche Felder:

json
"guppy-productcard-protective-frame": true,
"guppy-productcard-image-object-fit": "contain"

Login & Register / Checkout

twig
{% if theme_config('guppy-login-register') == "compact" %}
    {% set loginStyle = "compact" %}
{% endif %}

{% if theme_config('guppy-checkout') == "compact" %}
    {% set checkoutStyle = "compact" %}
{% endif %}

Template-Struktur

Ausgewählte Pfade unter src/Resources/views/storefront/:

text
storefront/
├── base.html.twig                              # Skip-Links + Layout
├── layout/
│   ├── header.html.twig
│   ├── header/
│   │   ├── header-compact.html.twig
│   │   ├── header-extended.html.twig
│   │   ├── header-simple.html.twig
│   │   ├── logo.html.twig
│   │   ├── search.html.twig
│   │   └── top-bar-extended.html.twig
│   ├── footer.html.twig
│   ├── footer/
│   │   └── footer-guppy-default.html.twig
│   ├── navbar/
│   │   ├── navbar.html.twig
│   │   ├── content.html.twig
│   │   └── content-compact.html.twig
│   ├── breadcrumb.html.twig
│   └── sidebar/
│       └── category-navigation-onelevel.html.twig
├── component/
│   ├── product/
│   ├── buy-widget/
│   ├── account/
│   ├── checkout/
│   ├── line-item/
│   ├── address/
│   ├── listing/
│   └── delivery-information.html.twig
├── element/                                    # CMS-Elemente
├── page/
│   ├── account/
│   ├── checkout/
│   ├── content/
│   └── product-detail/
├── section/
│   └── cms-section-sidebar.html.twig
├── block/
└── utilities/
    ├── alert.html.twig
    ├── icon.html.twig
    ├── offcanvas.html.twig
    └── thumbnail.html.twig

Vollständige Liste der überschriebenen Templates: Twig-Blöcke.

Custom Twig-Extensions

categoryOneLevel($navigationTree, $currentCategoryId)

Erlaubt einstufige Kategorie-Navigationen:

twig
{% set navigationData = categoryOneLevel(navigationTree, currentCategoryId) %}

{% for category in navigationData %}
    <a href="{{ category.url }}" class="nav-link">
        {{ category.name }}
    </a>
{% endfor %}

Implementierung:

php
// src/Twig/CategoryOneLevelExtension.php
class CategoryOneLevelExtension extends AbstractExtension
{
    public function getFunctions(): array
    {
        return [
            new TwigFunction('categoryOneLevel', [$this, 'categoryOneLevel'])
        ];
    }

    public function categoryOneLevel($navigationTree, $currentCategoryId)
    {
        // einstufige Kategorienavigation
    }
}

Barrierefreiheit

Konfigurierbar per theme_config:

twig
{# base.html.twig #}
{% block base_body_inner %}
    {% if theme_config('guppy-header-skip-to-main-content') %}
        <a href="#content-main" class="skip-link">
            {{ 'skipLink.toMainContent'|trans|sw_sanitize }}
        </a>
    {% endif %}

    {% if theme_config('guppy-header-skip-to-main-nav') %}
        <a href="#main-navigation" class="skip-link">
            {{ 'skipLink.toMainNavigation'|trans|sw_sanitize }}
        </a>
    {% endif %}

    {% if theme_config('guppy-header-skip-to-search') %}
        <a href="#search" class="skip-link">
            {{ 'skipLink.toSearch'|trans|sw_sanitize }}
        </a>
    {% endif %}

    {{ parent() }}
{% endblock %}

Verfügbare Skip-Link-Felder:

FeldWirkung
guppy-header-skip-to-main-contentSprung zum Hauptinhalt
guppy-header-skip-to-main-navSprung zur Hauptnavigation
guppy-header-skip-to-searchSprung zur Suche

USP-Banner

twig
{# layout/_includes/usp-banner.html.twig #}
{% if theme_config('guppy-usp-active') %}
    <div class="usp-banner" data-usp-banner-plugin="true">
        {% if theme_config('guppy-usp-layout') == 'benefits' %}
            {% for i in 1..4 %}
                {% set benefit = theme_config('guppy-usp-layout-benefit' ~ i) %}
                {% if benefit %}
                    <div class="usp-item">{{ benefit }}</div>
                {% endif %}
            {% endfor %}
        {% else %}
            <div class="usp-item">
                <a href="{{ theme_config('guppy-usp-layout-link') }}"
                   {% if theme_config('guppy-usp-layout-newTab') %}target="_blank"{% endif %}>
                    {{ theme_config('guppy-usp-layout-text') }}
                </a>
            </div>
        {% endif %}
    </div>
{% endif %}

Praktische Implementierung

Neuen Header-Typ

  1. Template anlegen:
twig
{# src/Resources/views/storefront/layout/header/header-custom.html.twig #}
{% sw_extends '@Storefront/storefront/layout/header/header.html.twig' %}

{% block layout_header_inner %}
    <div class="header-custom">
        {# Custom Header-Inhalt #}
    </div>
{% endblock %}
  1. Theme-Konfiguration erweitern:
json
"guppy-header": {
    "custom": {
        "options": [
            { "value": "custom", "label": { "de-DE": "Custom Header" } }
        ]
    }
}
  1. SCSS-Styles ergänzen:
scss
.header-variant-custom {
    .header-custom {
        // Custom Styles
    }
}

Neue Productcard-Variante

twig
{# src/Resources/views/storefront/component/product/card/box-custom.html.twig #}
{% sw_extends '@Storefront/storefront/component/product/card/box-standard.html.twig' %}

{% block component_product_box %}
    <div class="product-box box-custom">
        {# Custom Inhalt #}
    </div>
{% endblock %}
json
"guppy-productcard-config": {
    "custom": {
        "options": [
            { "value": "custom", "label": { "de-DE": "Custom Produktkarte" } }
        ]
    }
}

Alert-Varianten konfigurieren

twig
{# utilities/alert.html.twig #}
{% set alertClass = 'alert-' ~ type %}
{% if theme_config('guppy-alert-alert-outline-active') %}
    {% set alertClass = alertClass ~ ' alert-outline' %}
{% endif %}

<div class="alert {{ alertClass }}" role="alert">
    {{ message }}
</div>
json
"guppy-alert-color-success": "#D3F2E7",
"guppy-alert-color-success-text": "dark",
"guppy-alert-alert-outline-active": true

Best Practices

Vererbung sauber halten

twig
{# Immer von der entsprechenden Shopware-Vorlage erben #}
{% sw_extends '@Storefront/storefront/layout/header/header.html.twig' %}

{# Spezifische Blöcke überschreiben #}
{% block layout_header_logo %}
    <div class="header-logo-custom">
        {{ parent() }}
    </div>
{% endblock %}

Theme-Konfig konsumieren

twig
{# Variante setzen #}
{% if theme_config('guppy-component-variant') %}
    {% set variant = theme_config('guppy-component-variant') %}
{% endif %}

{# Fallback auf Default #}
{% if variant %}
    {# Custom Logik #}
{% else %}
    {{ parent() }}
{% endif %}

Semantik und A11y

twig
<nav role="navigation" aria-label="{{ 'navigation.main'|trans }}">
    {# Navigation-Inhalt #}
</nav>

<a href="#main-content" class="skip-link">
    {{ 'skipLink.toMainContent'|trans }}
</a>

Verwandt