How stages render
This page traces a stage from the storefront page load to the moment it’s mounted in the DOM. Useful when you’re debugging “why isn’t my popup appearing” or designing a custom theme integration.
The footer hook
Section titled “The footer hook”P2Lab Stage extends a single Shopware template: the storefront layout footer. The plugin adds a hidden context container to the existing layout_footer_inner_container block:
{% sw_extends '@Storefront/storefront/layout/footer/footer.html.twig' %}
{% block layout_footer_inner_container %} {{ parent() }} {% if p2labStageContext %} <div data-p2lab-stage data-p2lab-stage-context="{{ context|json_encode }}" hidden></div> {% endif %}{% endblock %}The p2labStageContext extension is populated by a subscriber that listens to every storefront page load and computes the current page’s identity (page type, product / category / manufacturer / CMS page IDs, request URL, configured GDPR mode, the route URLs the JS plugin needs to call).
No other base templates are overridden. Popups are not embedded in Twig — they’re fetched client-side from /p2lab-stage/resolve.
The JS plugin
Section titled “The JS plugin”P2LabStagePlugin is registered against the [data-p2lab-stage] selector. On every page where that container exists, Shopware’s PluginManager instantiates the plugin. The plugin then:
- Reads
data-p2lab-stage-contextinto a JS object. - Reads suppressed-popup IDs from
sessionStorage/localStorage(subject to GDPR mode — see below). - POSTs
/p2lab-stage/resolvewith the page context + suppressed IDs. - For each popup returned, schedules it according to its trigger.
Resolve endpoint
Section titled “Resolve endpoint”POST /p2lab-stage/resolve does the heavy lifting server-side.
Payload in
Section titled “Payload in”{ "pageType": "product", "requestUrl": "/product/spring-jacket", "productId": "<uuid>", "productIds": [], "categoryId": "<uuid>", "manufacturerId": "<uuid>", "productStreamIds": [], "cmsPageId": null, "clientSuppressedIds": ["<popup-uuid-1>", "<popup-uuid-2>"]}Sales channel and customer group are not sent — they’re read server-side from SalesChannelContext, which can’t be lied to from the client.
Filtering pipeline
Section titled “Filtering pipeline”Server-side, the plugin walks every active stage and applies these filters in order:
- Active toggle + schedule in range (
active_from/active_to). - Sales channel match.
- Customer group match (against the logged-in customer’s group, or the default group for anonymous).
- Rule evaluation (with
rule_operatorOR / AND). - Categories (with
includeChildrenper row, taking the category’s path into account). - Products / Manufacturers / Product streams / CMS pages matches.
- Page types + URL patterns with
pages_operator. - Client-suppressed IDs (popups the client says it’s already dismissed in this session).
- Server-side dismiss state (PHP session, when GDPR consent is missing for storage).
Surviving stages are sorted by priority.
Display mode and per-session cap
Section titled “Display mode and per-session cap”The remaining list is then capped by the system settings:
displayMode = only-first(default) — only the first (highest-priority) stage is kept; the rest are dropped for this page.displayMode = queue— up tomaxPerSessionstages are kept (or all of them ifmaxPerSession = 0).
This cap is a bandwidth optimization — the client also tracks its own session-wide distinct-popup counter.
Payload out
Section titled “Payload out”{ "popups": [ { "id": "<uuid>", "displayType": "modal", "triggerType": "delay", "triggerValue": 5, "frequencyType": "session", "closeButton": true, "overlayClose": true, "permanentDismissButton": false, "statsEnabled": true, "html": "<!-- pre-rendered stage HTML -->" } ], "displayMode": "only-first", "maxPerSession": 3}html is pre-rendered server-side from stage-content.html.twig — the client just inserts it into the DOM. Every layer’s positioning / styling / animation data lives in data-* attributes for the JS engines to read.
Trigger scheduling
Section titled “Trigger scheduling”For each popup in the resolve response:
immediate→ mount right away.delay→setTimeout(triggerValue × 1000)then mount.scroll→ scroll listener onwindow, fires at the threshold, removes itself.
When the trigger fires, the popup is appended to document.body, the entrance animation runs, the parallax / animation / countdown / coupon engines wire up, and an impression event is sent.
Event tracking
Section titled “Event tracking”While the popup is mounted, the client sends impression / CTA-click / close / permanent-dismiss events to POST /p2lab-stage/track:
{ "popupId": "<uuid>", "eventType": "impression", "pageType": "product", "requestUrl": "/product/spring-jacket"}If statsEnabled is false on the stage, the server silently no-ops (defensive backstop — the client already skips most calls).
On dismissal, the client also calls POST /p2lab-stage/dismiss to persist the suppression server-side (PHP session — useful when client storage is unavailable):
{ "popupId": "<uuid>", "frequencyType": "session", "frequencyValue": null }Unknown frequency types fall back to session so a misbehaving client can’t write arbitrary strings.
On consent change (visitor accepts cookies after a popup has been server-dismissed), the client calls GET /p2lab-stage/dismissed to fetch the server-side suppression list and migrate it into client storage.
CMS slider stages
Section titled “CMS slider stages”Slider stages don’t use the resolve pipeline. The CMS element p2lab-stage-slider renders inline via Shopware’s normal Shopping Experiences pipeline — the slider Twig partial is part of the page’s HTML response. The slider’s JS plugin (P2LabStageSliderPlugin, registered against [data-p2lab-stage-slider]) handles autoplay, transitions, swipe, dots, arrows.
Caching
Section titled “Caching”Every storefront route in the plugin is marked _httpCache: false — resolve / track / dismiss / dismissed must never be HTTP-cached because their responses vary per session, customer, and frequency state. The footer template itself stays cacheable because it only emits the static context container; the actual stage data is fetched dynamically.
Where to go next
Section titled “Where to go next”- Mouse parallax — the parallax engine details.
- CSS customization — what to override and how.
- GDPR & storage — where suppression state actually lives.
- Analytics → Events — what each event records.