Concepts
This page explains the ideas behind Ultimate Popups: how popups are loaded, what triggers and frequency capping mean at runtime, how targeting works, and what the cookie-consent registration does.
How a popup reaches the visitor
The plugin works in two phases per page load.
Phase 1 — server-side matching (PHP): On every storefront page load, the PopupSubscriber queries all active popups and evaluates each one against:
- The current sales channel.
- The targeting mode (page route or custom URL pattern).
- Shopware Rule Builder rules (all attached rules must match — AND logic).
Popups that pass all checks are added to the page as a ssd_popups extension on the page object.
Phase 2 — client-side loading (JavaScript): The Twig template (base.html.twig) renders one <div data-ssd-popup …> element per matched popup, carrying the popup's options as a JSON data attribute — including the resolved per-viewport matrix { desktop, tablet, mobile }. After the page loads, the SsdPopupPlugin JavaScript class reads each element, checks the frequency cap (browser storage), and — if the cap allows — applies any consent gate, then waits for the trigger condition, then fetches the popup content from the server via an XHR POST to /ssd-digital/popup/content/{id}.
The server renders the CMS page via PopupContentLoader, which selects the correct CMS page for the detected device and resolves all CMS slot data. The HTML response is inserted into a Bootstrap modal via Shopware's PseudoModalUtil.
This split means: the server decides which popups are eligible; the browser decides whether to show them and when.
Triggers
A trigger is the event that causes the popup to attempt to open (subject to the frequency cap).
Immediate
The simplest trigger. After page load, the JS plugin either opens the popup immediately or waits displayDelay seconds. No user interaction is required.
Exit Intent
The JS plugin attaches a mouseleave listener on document.documentElement. When the visitor moves the mouse cursor upward past the top edge of the page (event.clientY < 0), the listener fires, removes itself, and opens the popup. This fires only once per page load.
Exit intent is inherently a pointer-device pattern. The plugin detects touch-only devices via window.matchMedia('(hover: hover)'). On non-hover devices (most phones, many tablets) there is no cursor to track, so the plugin falls back to a timed display: it shows the popup after the active device's configured Delay, defaulting to 30 seconds when no delay is set (0 = immediate). To take full control on phones, unlock the Mobile tab's trigger and set Immediately with your own delay.
Frequency capping
The frequency cap is enforced entirely in the browser, before the XHR request is made. The JS plugin checks browser storage against the popup's ID:
| Frequency | Storage type | Key | Check |
|---|---|---|---|
| Always | — | — | Always passes. |
| Once per Session | session cookie | ssd_popup_<id> | Pass if cookie absent. |
| Once per Day | localStorage | ssd_popup_<id> | Pass if key absent or timestamp > 24 h old. |
| Once Ever | localStorage | ssd_popup_<id> | Pass if key absent. |
When the popup opens, _markAsShown() writes the key. On Always, nothing is written. On Once per Session, the value is a session cookie (no expiry) — shared across all tabs and windows and cleared when the browser closes; sessionStorage was deliberately avoided because it is per-tab and re-triggered the popup when a subpage was opened in a new tab. On Once per Day and Once Ever, the key persists across sessions in localStorage.
Implications:
- Caps are per browser profile, not per Shopware customer account.
- Private/Incognito mode starts with empty storage — a visitor using private mode will see the popup as if for the first time.
- Clearing browser data resets all caps for that browser.
Consent gating
A popup can be made to wait for cookie consent. The Required cookie consent field (per device) holds a list of cookie names; when it is non-empty the JS plugin will not arm the trigger until the visitor has accepted all of those cookies.
At load the plugin checks the relevant cookies via CookieStorage. If they are already accepted, the trigger arms normally. If not, the plugin subscribes to Shopware's CookieConfiguration_Update event (published when the visitor saves a cookie selection in the consent banner) and arms the trigger only once the required cookies are present. The trigger arms at most once.
This is for content that legally cannot load before consent — for example a popup that embeds a third-party newsletter or marketing widget. The selectable options come from Shopware's own registered cookie groups (the Required group is excluded, since those cookies are always set). Leave the field empty to show the popup regardless of consent.
This is distinct from the plugin's own cookie-consent registration, which simply declares the plugin's frequency-cap storage (a session cookie plus localStorage) to the Cookie Manager.
Page targeting
When a popup has no targeting mode set, it is eligible on every storefront page.
Page Restriction mode uses a static mapping from human-readable route keys (home, product, category, …) to Shopware route names (frontend.home.page, frontend.detail.page, frontend.navigation.page, …). The subscriber reads the current request's _route attribute and compares it to the popup's target_routes array. At least one key must match.
Custom URL mode works on the raw request path (the SEO-friendly URL, not the internal rewritten path). The subscriber strips the sales-channel base URL prefix, then tests each stored pattern against the path using a regex that treats * as .*. The match is case-insensitive and trailing-slash-tolerant.
Category restriction is a sub-filter of Page Restriction. It applies only when the current route is frontend.navigation.page (a category/listing page). The subscriber reads the navigationId from the request and checks whether it appears in the popup's categories collection. If no categories are attached to the popup, all category pages pass.
Product restriction mirrors the category filter for product pages. It applies only when the current route is frontend.detail.page (a product detail page). The subscriber reads the productId from the request and checks whether it appears in the popup's products collection. A variant page carries the variant's own id, so a popup pinned to a specific variant fires only there. If no products are attached to the popup, all product detail pages pass.
Per-viewport settings and inheritance
Almost every display setting is configurable per device — CMS page, modal size, vertical alignment, hide-header, light close icon, trigger, delay, required cookie consent, and custom CSS. Desktop is the base; Tablet and Mobile inherit it until a field is unlocked. Storage uses nullable tablet_*/mobile_* override columns where NULL means "inherit Desktop" (a flat coalesce — no tablet→mobile cascade). See the configuration reference for the column map.
The split of responsibilities is deliberate: PHP resolves inheritance only (it ships an effective { desktop, tablet, mobile } matrix with no NULLs), and the JavaScript decides which device the visitor is on, reading the theme's --sw-current-breakpoint (XS/SM → mobile, MD → tablet, LG/XL/XXL → desktop). Because the breakpoint authority is the theme's own SCSS, the popup stays correct even when a theme moves its breakpoints.
The JS plugin subscribes to Shopware's Viewport/hasChanged event. When the viewport class changes while the popup is open (resize or device rotation), the plugin re-applies the new device's presentational settings — and, for CMS content, fetches the matching CMS page (via the device parameter in the XHR request, falling back to the desktop page when a variant is unassigned) and swaps the modal body without closing the popup. The display trigger, however, is evaluated once at load for the load-time device and is not re-armed on resize — a trigger is about the entry moment.
Custom CSS is injected into a <style> element and automatically wrapped in a per-popup, per-device scope, so a merchant's selectors only affect that single popup on the active device; the wrapped string is swapped on breakpoint change.
HTTP caching
The plugin is cache-aware. The PopupSubscriber dispatches AddCacheTagEvent with a global tag (ssd_popup-id) and per-popup tags (ssd_popup-<id>) for each popup that matched the current page. This tells Shopware's HTTP cache which cache entries are associated with which popups.
When a popup is saved, the PopupCacheInvalidationSubscriber listens for EntityWrittenContainerEvent, extracts the IDs of written popups, and calls CacheInvalidator::invalidate() with the corresponding tags. This ensures that pages showing an edited popup are immediately invalidated and rebuilt.
Cookie consent registration
The plugin registers itself in the Shopware Cookie Manager to be transparent about its browser storage use. It registers under the Required group (not Optional) because the frequency-capping data — a session cookie for "once per session" and localStorage for "once per day"/"once ever" — is necessary for the plugin's core behaviour. Without it, those caps would not function.
The registration appears in the Cookie Manager as "Ultimate Popup" and is associated with the cookie identifier ssd_ultimate_popup. GDPR-friendly shops that use Shopware's consent system will surface this entry to visitors in the cookie preference dialog.
Rule Builder integration
The plugin reuses Shopware's existing Rule Builder without adding any custom rule conditions. Any rule created in Settings > Rules can be attached to a popup.
Rules are evaluated server-side: the PopupSubscriber compares the popup's attached rule IDs against the SalesChannelContext::getRuleIds() list (which Shopware pre-evaluates for the current visitor and request context). All attached rules must appear in the context's active rule list — this is AND logic. A popup with no rules attached matches all visitors.
This means you can target popups with any combination of: cart value, customer group, country, language, browser, operating system, time window, or any other condition that Shopware's Rule Builder supports.
