Skip to content

Twig Overrides

Guppy follows a component-based templating approach: dynamic, modular templates driven by theme.json and cleanly decoupled from Shopware defaults. The goal is update-friendliness without patching core code.

Why this approach?

  1. Maximum customisability: configuration values instead of code changes.
  2. Modularity: components (footer, header, widgets) as standalone templates.
  3. Upgrade compatibility: override via sw_extends, no core touches.
  4. Accessibility: modular templates allow targeted a11y improvements.

Theme integration via theme.json

Fields from theme.json are read in Twig via theme_config('field-name'). Example: footer variant.

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 %}

Matching theme.json field:

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": { "en-GB": "Shopware Default" } },
        { "value": "guppy-default", "label": { "en-GB": "Guppy Default" } }
      ]
    },
    "editable": true,
    "tab": "footer",
    "block": "layoutFooter",
    "order": 100
  }
}

Variant template:

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

{# Content here #}

The variant system

Guppy applies the variant pattern consistently. Examples:

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 %}

Available variants: default, compact (Guppy default), extended (with 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 %}

Available variants: content, content-compact.

Product cards

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 %}">
    <!-- Product card content -->
</article>

Available variants: default, guppy-default (accessible). Additional fields:

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 structure

Selected paths under 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 elements
├── 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

Full list of overridden templates: Twig Blocks.

Custom Twig extensions

categoryOneLevel($navigationTree, $currentCategoryId)

Renders single-level category navigations:

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

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

Implementation:

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

    public function categoryOneLevel($navigationTree, $currentCategoryId)
    {
        // single-level category navigation
    }
}

Accessibility

Configurable via 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 %}

Available skip-link fields:

FieldEffect
guppy-header-skip-to-main-contentJump to main content
guppy-header-skip-to-main-navJump to main navigation
guppy-header-skip-to-searchJump to search

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 %}

Practical implementation

A new header type

  1. Create the template:
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 content #}
    </div>
{% endblock %}
  1. Extend the theme configuration:
json
"guppy-header": {
    "custom": {
        "options": [
            { "value": "custom", "label": { "en-GB": "Custom Header" } }
        ]
    }
}
  1. Add SCSS:
scss
.header-variant-custom {
    .header-custom {
        // Custom styles
    }
}

A new product card variant

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 content #}
    </div>
{% endblock %}
json
"guppy-productcard-config": {
    "custom": {
        "options": [
            { "value": "custom", "label": { "en-GB": "Custom product card" } }
        ]
    }
}

Alert variants

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

Inheritance done right

twig
{# Always extend the matching Shopware template #}
{% sw_extends '@Storefront/storefront/layout/header/header.html.twig' %}

{# Override only the specific blocks #}
{% block layout_header_logo %}
    <div class="header-logo-custom">
        {{ parent() }}
    </div>
{% endblock %}

Consume theme config

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

{# Fallback to default #}
{% if variant %}
    {# Custom logic #}
{% else %}
    {{ parent() }}
{% endif %}

Semantics and a11y

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

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