tecnostack / module-cookie-consent
Cookie banner and privacy preference centre for Magento 2 with Google Consent Mode v2, script/iframe/pixel blocking, a GTM dataLayer stream and a server-side consent log. Theme independent (Luma, Hyva, custom themes), RTL ready.
Package info
github.com/Sayed-Kerkesh/magento2-cookie-consent
Type:magento2-module
pkg:composer/tecnostack/module-cookie-consent
Requires
- php: ~7.4.0||~8.0.0||~8.1.0||~8.2.0||~8.3.0||~8.4.0
- magento/framework: *
- magento/module-backend: *
- magento/module-cms: *
- magento/module-config: *
- magento/module-cookie: *
- magento/module-customer: *
- magento/module-media-storage: *
- magento/module-store: *
- magento/module-theme: *
- magento/module-ui: *
- magento/module-widget: *
Requires (Dev)
None
Suggests
- magento/module-csp: Inline scripts and styles are rendered through SecureHtmlRenderer and receive the CSP nonce when Magento_Csp is enabled.
- magento/module-google-adwords: Optional blocking of Magento's Google Ads conversion tag by consent category.
- magento/module-google-analytics: Optional blocking of Magento's Google Analytics tag by consent category (2.4.0 - 2.4.4).
- magento/module-google-gtag: Optional blocking of Magento's GA4 / Google Ads tags by consent category (2.4.5+).
Provides
None
Conflicts
None
Replaces
None
README
TecnoStack Cookie Consent for Magento 2
A consent banner and privacy preference centre that drops into any Magento 2 theme.
Google Consent Mode v2 · script, iframe and pixel blocking · server-side consent log · Arabic and English
Features · Storefront · Admin · Quick start · Documentation · Changelog
✨ Features
| 🎨 Any theme The UI lives in an isolated Shadow DOM: theme CSS cannot break it, and it cannot break the theme. Restyle it from the admin or with ::part(). |
⚡ Cache friendly Full page cache and Varnish safe. Every visitor gets the same HTML; every decision is taken in the browser. |
🛡️ Nothing runs before consent Scripts, iframes and pixels wait for their category: declarative markup, URL patterns and Magento's own Google tags. |
📈 Google Consent Mode v2default before any tag, update after every choice, plus a GTM dataLayer stream and an optional OneTrust shim. |
🌍 Arabic and English Right to left mirrored from the store locale, even when the theme never sets dir="rtl". Every text is translatable per store view. |
🧾 Consent log A server-side record of every choice, with hashed IPs, retention clean-up, filters and CSV export. |
| 🔒 CSP safe One tiny inline script through SecureHtmlRenderer; colours and custom CSS go through CSSOM, never inline styles. |
🧩 Widgets A Cookie Table for your cookie policy page and a Cookie Settings Link for any footer, CMS page or block. |
🪶 Dependency free Plain JavaScript: no jQuery, RequireJS, Knockout or Alpine. PHP 7.4 to 8.4 from the same code. |
🖥️ Storefront
Stock Luma theme · English and Arabic · desktop and mobile
🛠️ Admin
Browse every admin screen
🚀 Quick start
composer require tecnostack/module-cookie-consent bin/magento module:enable TecnoStack_CookieConsent bin/magento setup:upgrade bin/magento cache:flush
The banner shows on the next page load with four default categories and a starter cookie registry. Then work through the post-install checklist: store name, cookie policy page, your real cookies and your tags.
📚 Documentation
1. Compatibility
| Supported | |
|---|---|
| Magento Open Source / Adobe Commerce | 2.4.3 – 2.4.8 |
| PHP | 7.4, 8.1, 8.2, 8.3, 8.4 |
| Themes | Luma, Blank and custom themes (tested on Luma and a commercial theme); Hyvä and Breeze by design: Shadow DOM UI, dependency-free script, Hyvä layout handle included |
| Full page cache | Built-in and Varnish |
| Browsers | Current Chrome, Edge, Safari, Firefox; Shadow DOM is attached by script where declarative Shadow DOM is not supported |
| Headless / PWA Studio | Not rendered automatically; the JavaScript API and the log endpoint can be reused |
Magento_Csp, Magento_GoogleGtag, Magento_GoogleAnalytics and Hyvä are optional (soft integrations).
2. Installation
# Option A: composer (artifact or private repository) composer require tecnostack/module-cookie-consent # Option B: copy the module mkdir -p app/code/TecnoStack/CookieConsent cp -R <source>/* app/code/TecnoStack/CookieConsent/ bin/magento module:enable TecnoStack_CookieConsent bin/magento setup:upgrade bin/magento setup:di:compile # production mode bin/magento setup:static-content:deploy ar_SA en_US -f # your locales bin/magento cache:flush
Installation creates five tables, the four default categories (IDs 1-4, English labels plus Arabic labels for every store view with an Arabic locale) and a starter cookie registry.
Post-install checklist
- Stores > Configuration > TecnoStack > Cookie Consent: set the Store display name per store view
(it replaces
{{store_name}}) and pick the Cookie policy page. - Create or translate the cookie policy CMS page and insert the Cookie Table widget.
- Review the category texts per store view in Content > Cookie Consent > Categories, especially the second Targeting paragraph (social partners / joint responsibility).
- Fill the Cookie Registry with the cookies your store really sets (DevTools > Application, or a
scanner). Names ending with
*are wildcards (_ga_*). - Hold back third-party snippets: mark them with
type="text/plain" data-ts-category="..."or add URL patterns to auto-blocking (section 6). - GTM: create the
tsActiveGroupsvariable and the consent triggers (section 7), or enable the OneTrust shim when migrating from OneTrust. - Verify in GTM Preview / Tag Assistant that the Consent Mode
defaultandupdatecommands fire. - Make sure a "Cookie settings" link is visible in your footer (section 5).
- After any change that needs fresh consent, increase Consent version.
3. How it works
| Moment | What happens |
|---|---|
| First visit (implied / notice model) | All active categories are granted by default, ts_consent is written with ic=0, the banner is shown. |
| First visit (explicit model) | Only required categories are granted; nothing optional runs; no consent cookie is written. |
| Accept / Allow all / Confirm my choices | ts_consent and ts_consent_closed are written, blocked content of granted categories is released, Consent Mode is updated, dataLayer and DOM events are fired, the choice is logged. |
| A category is revoked | Its registry cookies are deleted, Consent Mode is updated and, when code of that category already ran, the page reloads once without it. |
| Consent version changed | The banner shows again and the previous choice no longer applies. |
Cookies set by the module
| Cookie | Content |
|---|---|
ts_consent |
v=1&id=<uuid>&groups=1%3A1%2C2%3A1%2C3%3A0%2C4%3A1&ts=<ISO>&ic=1&model=implied&store=ar |
ts_consent_closed |
ISO timestamp, written once the visitor acted on the banner |
user_allowed_save_cookie |
Magento's own flag, so wishlist, compare and recently viewed keep working. Written on the first visit in the implied and notice models, and only after the visitor's choice in the explicit model |
Attributes: path=/, SameSite=Lax, Secure on HTTPS, Magento's cookie domain when it matches the host,
lifetime = "Consent lifetime (days)".
4. Configuration reference
Stores > Configuration > TecnoStack > Cookie Consent (tecnostack_cookieconsent/*). Every text is
store-view scoped; empty texts use the translated defaults (English and Arabic are included).
| Group | Main fields |
|---|---|
| General | Enable, Consent model (implied / explicit / notice), Consent version, Consent lifetime, cookie names, Store display name, Cookie policy page, URL override, Disable Magento native cookie notice, Excluded paths |
| Banner | Position (bottom-start / bottom-end / bottom-center / bottom-full-width), title, text, three bullets, button labels |
| Preference Centre | Drawer side (left / right / inline-start / inline-end), Close on overlay click, Show "Allow all", Show cookie list per category, texts, footer link options |
| Design | Primary button colours, accent, toggle OFF, border, expanded row background, overlay opacity, custom CSS |
| Integrations | Consent Mode v2, wait_for_update, regions, dataLayer events and variable name, url_passthrough, OneTrust shim, Google tag handling, auto-block patterns |
| Consent Log | Enable, log implied defaults, hashed IP, user agent, retention (days) |
Admin pages: Content > Cookie Consent > Categories / Cookie Registry / Consent Log / Settings.
ACL resources: TecnoStack_CookieConsent::categories, ::cookies, ::logs, ::logs_export, ::config.
5. Theme integration (any theme)
The banner and preference centre
Nothing to do: the UI is injected in after.body.start and isolated in a Shadow DOM. It inherits only the
theme's font family and the text direction of the store locale (RTL for Arabic, Persian, Hebrew, Urdu...),
even when the theme does not set dir on <html>.
Restyle it without touching the theme:
- Admin > Design: colours and overlay opacity, applied as CSS custom properties.
- Admin > Design > Custom CSS: rules applied inside the Shadow DOM, e.g.
.tscc-banner { border-radius: 8px; }. - From the theme, through
::part():
#ts-cookie-consent::part(banner) { border-radius: 12px; } #ts-cookie-consent::part(button) { text-transform: uppercase; } #ts-cookie-consent::part(drawer) { width: 420px; }
Parts: root, banner, banner-body, banner-title, banner-actions, button, accept-button,
settings-button, ok-button, overlay, drawer, drawer-title, close-button, more-info-link,
allow-all-button, accordion, always-active, switch, drawer-footer, confirm-button,
placeholder, placeholder-button.
The "Cookie settings" footer link
The preference centre must stay reachable after the first choice. Pick what fits the theme:
| Theme | How |
|---|---|
| Luma, Blank and themes based on them | Automatic: added to the footer_links block ("Footer link: add automatically" = Yes). |
| Hyvä | Automatic: hyva_default.xml moves the link into the Hyvä footer container. |
| Any other theme | Footer link: CSS selector — the link is appended to every element matching the selector, e.g. .page-footer .footer-links ul or .page-footer ul:has(a[href*="privacy"]) (inside a list it becomes a list item). |
| Anywhere (CMS) | The Cookie Settings Link widget: {{widget type="TecnoStack\CookieConsent\Block\Widget\SettingsLink" label="Cookie settings"}} |
| Anywhere (code) | Any element with the data-ts-cookie-settings attribute, or TsConsent.openPreferences(). |
The Cookie Table widget
Insert it in any CMS page or block: Insert Widget > Cookie Table (with Page Builder, inside an
HTML Code element). The table keeps the theme's own table styles (data table, stacked rows with
data-th labels on mobile). The module only pins the cell alignment, through a small stylesheet linked
next to the table (css/cookie-table.css), because Magento's table library sets
th { text-align: left } even in RTL stores. Latin values such as 2 years or _ga_* are wrapped in
<bdi>, so they keep their order in Arabic store views; this applies to the cookie list in the preference
centre too.
6. Blocking scripts, iframes and pixels
Declarative markup (recommended)
<script type="text/plain" data-ts-category="performance" src="https://www.clarity.ms/tag/XXXX"></script> <script type="text/plain" data-ts-category="targeting"> !function(f,b,e,v,n,t,s){/* Meta Pixel */}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js'); </script> <iframe data-ts-category="functional" data-src="https://www.youtube-nocookie.com/embed/ID" title="Video"></iframe> <img data-ts-category="targeting" data-src="https://www.facebook.com/tr?id=XXXX&ev=PageView&noscript=1" alt="" width="1" height="1">
- Scripts are re-created in document order; inline code runs after the external file it depends on.
data-ts-category="performance,targeting"releases only when all listed categories are granted.data-ts-type="module"restores a non-classic type.- Blocked iframes show a placeholder ("This content is blocked by your cookie settings.") with a Cookie settings button; granting the category loads the iframe without a reload.
Auto-blocking by URL pattern
For code injected by modules you cannot edit, add one pattern|categoryCode per line:
connect.facebook.net|targeting
analytics.tiktok.com|targeting
sc-static.net|targeting
clarity.ms|performance
hotjar.com|performance
- Server side: matching
<script src>tags, inline scripts andtext/x-magento-initcomponents are rewritten to the blocked format in every storefront HTML response. - Client side: a guard in the head script holds back matching scripts inserted later by other scripts.
- While no pattern is configured, neither runs.
Remember to allow the hosts of tags you release in your project's csp_whitelist.xml when Magento_Csp
is enabled.
Magento's own Google tags
By default ("Let Consent Mode govern Google tags" = Yes) Magento's Google Analytics / Google Ads tags load
normally and obey the Consent Mode signals. Set it to No and "Block Magento Google tags by category" =
Yes to hold them back instead (GA with performance, Google Ads with targeting). Magento 2.4.5+ renders
the Google Ads snippets with plain template blocks; they are matched by layout name
(gtag_google_adwords, gtag.google.adwords). Add other blocks through di.xml:
<type name="TecnoStack\CookieConsent\Observer\BlockNamedTagsObserver"> <arguments> <argument name="blockCategories" xsi:type="array"> <item name="my.pixel.block" xsi:type="string">targeting</item> </argument> </arguments> </type>
Strict CSP (Magento 2.4.7+ checkout): a released script keeps only the nonce it was rendered with,
so injected markup can never borrow the page nonce. Render your own blocked inline scripts through
$secureRenderer->renderTag('script', ['type' => 'text/plain', 'data-ts-category' => '...'], $js, false)
so they carry the nonce on those pages.
7. Google Consent Mode v2 and GTM
The head script is printed right after <meta charset>, before every other script in <head> (including
a GTM snippet pasted into Content > Design > Configuration > HTML Head > Scripts and Style Sheets), and
sends:
gtag('consent', 'default', { ad_storage, ad_user_data, ad_personalization, // targeting analytics_storage, // performance functionality_storage, personalization_storage, // functional security_storage: 'granted', wait_for_update: 500 });
Implied model: granted until the visitor opts out. Explicit model: denied until the visitor consents.
After every choice: gtag('consent', 'update', {...}). The mapping is editable per category
(Content > Cookie Consent > Categories > Google Consent Mode types). A type mapped to several categories
is granted only when all of them are granted.
dataLayer events
| Event | When | Payload |
|---|---|---|
ts_consent_ready |
Every page | ts_consent: {performance, functional, targeting, version, interacted}, ts_active_groups |
ts_consent_update |
After Accept, Allow all, Confirm or TsConsent.reset() |
same + ts_consent_action |
OneTrustGroupsUpdated |
Same moments, OneTrust shim only | OnetrustActiveGroups |
GTM setup
- Variable TS Active Groups: type JavaScript Variable, name
tsActiveGroups(e.g.,1,2,4,). - Trigger TS - Performance granted: Custom Event, event name
ts_consent_ready|ts_consent_update(use regex matching), fires whenTS Active Groupscontains,2,. Create the same for,3,(functional) and,4,(targeting). - Attach non-Google tags (Meta, TikTok, Snap, Clarity...) to those triggers. Google tags follow Consent Mode on their own.
- Migrating from OneTrust: enable OneTrust compatibility shim; existing triggers on
OnetrustActiveGroupsandOneTrustGroupsUpdatedkeep working.
8. JavaScript API
TsConsent.getConsent(); // {version, id, groups: {necessary: true, performance: false, ...}, ts, interacted, model} TsConsent.has('targeting'); // boolean TsConsent.onConsent('performance', fn); // runs fn now if granted, or once when granted later TsConsent.onChange(fn); // after every save, with the consent object TsConsent.openPreferences(); // opens the preference centre TsConsent.acceptAll(); TsConsent.save({performance: true, functional: false, targeting: true}); TsConsent.reset(); // deletes both consent cookies and shows the banner again document.addEventListener('ts:consent:update', (event) => console.log(event.detail));
window.tsActiveGroups is available synchronously from the head script (before GTM loads).
9. Consent log
- Every Accept / Allow all / Confirm / withdraw is posted to
POST /cookieconsent/log/save(fetch withkeepalive,navigator.sendBeaconfallback). The endpoint validates Magento's form key, whitelists every field, rejects bodies over 4 KB, allows 20 writes per consent ID and 300 per client IP per hour, and is never cached. - Stored: consent ID, store view, customer ID (when logged in), action, category states, version, model,
page path (no query string), optional salted SHA-256 of the IP (
crypt/keyis the salt), optional user agent. - Content > Cookie Consent > Consent Log: filters, full-text search on the consent ID, mass delete,
CSV / Excel XML export (ACL
TecnoStack_CookieConsent::logs_export). - Retention: a daily cron job (
15 3 * * *) deletes records older than "Retention (days)" (default 1095). - Erasure request:
bin/magento tecnostack:cookie-consent:purge --customer-id=123
bin/magento cron:run --group=default # runs the retention job
Behind a load balancer or CDN, configure Magento's Magento\Framework\HTTP\PhpEnvironment\RemoteAddress
alternativeHeaders (di.xml) so the real client IP is hashed.
Extending: decorate or replace TecnoStack\CookieConsent\Api\ConsentLoggerInterface (di.xml plugin or
preference) to forward records to a CRM or a cross-device consent store.
10. Consent models and per-country rules
| Model | Before the visitor chooses |
|---|---|
implied (default) |
All categories active; toggles ON |
explicit |
Only required categories active; optional toggles OFF |
notice |
All active; the banner shows a single "OK" button |
TecnoStack\CookieConsent\Api\ConsentModelResolverInterface::resolve(int $storeId) decides the model; the
default implementation returns the store view setting. A resolver that varies per request (for example by
CF-IPCountry) must also vary the full page cache on the same input (HTTP context / Varnish VCL), because
the decision is embedded in the cached page.
11. Translations
i18n/en_US.csv and i18n/ar_SA.csv hold every storefront default. Magento's translation order applies:
module < language pack < theme < inline translation, so a language pack that already translates generic
words ("Back", "More information", "OK") wins. Set the exact wording in the admin text fields when needed.
12. Performance and caching
- No layout handle is uncacheable; the consent cookie is never part of the cache key.
- Banner, preference centre and JSON config are cached in
block_htmland the full page cache with the tagtscc_config, which is purged (built-in FPC and Varnish) when the configuration, a category or a registry entry changes. - The banner is
position: fixed: no layout shift. - Assets: one deferred script and one stylesheet loaded inside the Shadow DOM.
13. Security and privacy
- All output is escaped; category descriptions allow only
p,br,strong,a,ul,li. - Custom CSS cannot close its style element (
<is removed) and is applied through CSSOM. - No personal data in cookies; the consent ID is a random UUID v4.
- Admin controllers are ACL protected; the log endpoint is CSRF protected and rate limited.
14. Testing
# Unit (from the Magento root) vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist app/code/TecnoStack/CookieConsent/Test/Unit # Integration (requires dev/tests/integration/etc/install-config-mysql.php) cd dev/tests/integration && ../../../vendor/bin/phpunit ../../../app/code/TecnoStack/CookieConsent/Test/Integration # End to end (Playwright): store views /ar/ and /en/, desktop and mobile cd app/code/TecnoStack/CookieConsent/Test/E2E npm install && npx playwright install chromium TSCC_BASE_URL=https://your-store.test npx playwright test
The E2E suite (E1-E17) adds its own fixtures to the page (blocked scripts and a blocked iframe) and simulates a consent version bump or the explicit model by rewriting the frontend config in the response, so it needs no admin or database changes.
15. Troubleshooting
| Symptom | Check |
|---|---|
| No banner | Module enabled for the store view? Excluded path? Already consented (delete ts_consent_closed)? |
| Banner unstyled for a moment | The stylesheet is fetched by the Shadow DOM; make sure static content is deployed for the theme/locale. |
| No "Cookie settings" link | Theme without footer_links: set "Footer link: CSS selector", or use the widget (section 5). |
| Blocked script never runs | data-ts-category must contain existing category codes; a typo keeps it blocked (safe default). |
| Consent log 403 | The request must carry Magento's form_key cookie; check that no proxy strips cookies on POST. |
| GA/Ads not blocked | "Let Consent Mode govern Google tags" is Yes by default; Consent Mode is the mechanism then. |
16. Uninstall
bin/magento module:disable TecnoStack_CookieConsent bin/magento setup:upgrade
Remove the code (or composer remove tecnostack/module-cookie-consent). The tables
tecnostack_cookie_* are kept with your consent records; drop them manually if you no longer need them.
17. Legal note
The default implied (opt-out) model mirrors the reference storefront in Saudi Arabia, where optional
categories are active until the visitor turns them off. Saudi Arabia's Personal Data Protection Law (PDPL),
the UAE PDPL, the GDPR / ePrivacy rules for EU visitors and other laws may require explicit opt-in for some
uses. Use the explicit model, or a ConsentModelResolverInterface per-country rule, where required, and
have your legal adviser approve the final texts. This module is a technical tool, not legal advice.
















