
A step-by-step guide to wiring up GTM, GA4, Hotjar, and Bloomreach Engagement so they only fire after consent, and fixing the consent-signal mismatch Framer’s defaults don’t cover.
This is technical implementation guidance for enforcing consent choices, not legal advice. GDPR obligations vary by jurisdiction and use case; confirm your setup with qualified counsel.
The problem
Framer has a built-in cookie banner. It looks compliant, it has a Reject all button, consent categories, and a Privacy Policy link. But out of the box it ships two defaults that stop it from technically enforcing consent the way GDPR expects:
Problem 1. Returning visitors are never tracked.
Framer stores the visitor’s choice in localStorage and replays it on the next visit as a consent default, not a consent update. It does that after React hydration, several hundred milliseconds in. By then your own head snippet has already set a deny-first default and loaded GTM, and gtag ignores a default that arrives after the container has initialized. The consent state stays denied for the whole visit. The visitor consented on their last visit, the banner does not reappear, and nothing fires. Step 3 is the fix.
Problem 2. Reject all denies the two signals the banner promised would stay on.
Framer’s banner settings let you mark Necessary cookies as always active and not revocable, and the banner UI renders that correctly. The consent signals do not match. When a visitor who has already consented opens Cookie Settings and clicks Reject all, Framer pushes functionality_storage: denied and security_storage: denied alongside the tracking signals. These two cover basic site operation (language preferences, UI state) and session integrity, which is exactly what the banner promised would stay on. Framer also writes necessary: false into its stored consent. Any tool that respects those two signals now behaves as if Necessary was revoked.
This is not limited to a revoke. A first-time visitor who reads the banner and clicks Reject all lands in the same state, because Framer’s own defaults start necessary at true and the click writes it to false. We watched localStorage flip across that single click on September 6, 2026:
The visitor is never offered a control for that category. There is no toggle, only the words Always active. A separate manual has the full behavior across twenty-six tested states: Framer’s Cookie Banner Denies the Cookies It Calls Always Active.
This guide walks through the exact GTM setup that fixes both problems.
What you need
Framer site with a custom domain
Google Tag Manager (GTM)
GA4 connected via GTM
Hotjar (optional but used as an example)
Bloomreach Marketing (Engagement) / Exponea (optional but used as an example)
The pattern works for any analytics or personalization tool. Hotjar and Bloomreach are just the examples we use here.
Step 1: Add Framer’s cookie banner
Framer ships the banner as a component, not a site setting. Insert the Cookie Banner component onto a layout every page shares, such as your footer, then select it and put your GTM container ID in the GTM ID field in the properties panel. That field does more than label the banner, and the section after Step 2 explains what.
Configure four categories:
Category | Description | Toggle |
|---|---|---|
Necessary | Security and basic functionality | Always active (locked) |
Preferences | Personalized content and settings | Off by default |
Analytics | Performance tracking | Off by default |
Marketing | Ads personalization and tracking | Off by default |
Set the banner to show on first visit with Reject all, Customize, and Accept all options.
Step 2: Set consent defaults before GTM loads
In Framer go to Site Settings → Custom Code → Head.
Add this before your GTM snippet:
This runs before GTM loads and sets everything consent-gated to denied by default, while keeping functionality and security granted so basic site operation works before the user interacts with the banner. GTM will respect these defaults and block any tags that require consent. (personalization_storage is the seventh Consent Mode v2 signal, covering video recommendations and similar personalization; include it in defaults for completeness.)
Then add your GTM snippet immediately after:
Replace YOUR-GTM-ID with your GTM container ID.
Why you end up with two GTM containers
Framer’s cookie banner does not only read consent. Give the component a GTM ID and it loads GTM itself: it pushes a consent default built from whatever it finds in localStorage, then injects its own gtm.js script tag. That happens after React hydration, which on our pages lands between 480ms and 1130ms into the load.
So once you add the head snippet from Step 2, the same container is requested twice. It looks alarming in DevTools and it is not a bug. GTM initializes a container once, keyed by container ID, and the second request is a cache hit. We measured it on our own site on September 6, 2026: first request 773ms of network time, second request 9ms from cache, one page_view, one scroll, no double counting.
The obvious fix is the wrong one. Clearing the GTM ID on the banner component does remove the second load. It also removes every consent signal. In Framer’s bundled component the whole consent path is guarded by that ID being set, so no ID means no consent default, no consent update, and no cookie_consent_update event. The banner still renders and still writes the visitor’s choice to localStorage. GTM never hears about any of it, and every tag with a consent check stays blocked forever, including for visitors who clicked Accept all. Nothing in the interface tells you this happened.
That leaves two configurations that work:
Setup |
| GTM live at | Deny-first default |
|---|---|---|---|
Head snippet + banner | 2 (second from cache) | 85ms to 205ms | Fixed value in the HTML |
Banner only, no head snippet | 1 | 480ms to 1130ms | Rebuilt from |
Both gate a first-time visitor correctly. The banner pushes its default before it injects the container, so nobody is tracked without consent either way. They do not both survive a revoke, and that difference is permanent rather than a matter of milliseconds. Step 5 has the measurement.
We kept the head snippet. The 400ms to 900ms it buys back is the window in which a fast-bouncing visitor gets measured at all, a default hardcoded in the HTML does not depend on a React component reaching the point where it can push one, and it is the only thing standing between a visitor who revokes and a permanently wrong consent state.
Count your own loads in the console: document.querySelectorAll('script[src*="gtm.js"]').length. Two is expected with the head snippet in place.
Step 3: Replay the stored consent at Consent Initialization
This is the fix for Problem 1, and it is the piece most Framer setups are missing. Framer replays a returning visitor’s stored choice as a consent default. Your head snippet already issued a default and loaded the container, so gtag discards Framer’s. Nothing tells you this happened. The banner stays hidden, the visitor looks consented, and every gated tag sits blocked for the entire session.
Read the stored choice yourself and push it as an update, which gtag always honors, at the earliest point GTM offers.
Create a new tag in GTM:
Type: Custom HTML
Name:
Consent Restore — Framer Stored ConsentTrigger: Consent Initialization - All Pages
HTML:
Framer stores four booleans (analytics, marketing, necessary, preferences). This maps them onto the five gated Consent Mode signals and grants the two necessary ones outright. Granting them here is deliberate. Your head snippet already grants them, so on a normal page load these two lines change nothing. They matter on the page loads where the head snippet is not there to help, and they make the container self-sufficient rather than dependent on a second piece running first. The cookie_consent_update push is what releases the tags in Step 4, and it is guarded so a stored rejection does not fire it.
Consent Initialization runs before every other trigger in the container, which is the point. We measured this on our own site on September 6, 2026. For a returning consented visitor the restored update landed before 115ms, because the Bloomreach loader that listens for it requested exponea.min.js at 115ms. Framer’s own replay did not arrive until 976ms, and gtag ignored it when it did.
If you run the banner without a head snippet, Framer’s default is the first one the container sees, so it is honored and Problem 1 does not arise. Keep the tag anyway. Its last two lines are then the only thing granting the necessary signals on a page load where the visitor has declined, and nothing else is there to do it. That configuration has its own costs, covered in the section above.
Step 4: Create the two consent triggers in GTM
You need two triggers that everything runs through:
Trigger 1. DOM Ready
Type: DOM Ready
Fires on: All Pages
Name it:
DOM Ready
This handles returning visitors. By the time DOM Ready fires, Framer has already loaded the stored consent into the dataLayer.
Trigger 2: cookie_consent_update
Type: Custom Event
Event name:
cookie_consent_updateFires on: All Custom Events
Name it:
cookie_consent_update
This handles new visitors. Framer pushes this event when a user makes a consent choice. It also fires for returning visitors when Framer loads stored consent on page load.
Both triggers are load-bearing. It’s tempting to collapse them into one, and it doesn’t work. We tested this on our own container on September 6, 2026. A tag on DOM Ready alone fired once for a returning visitor and zero times for a new visitor who accepted the banner, because that consent arrives after DOM Ready has already passed. Moving the tag to a single earlier trigger is worse: on All Pages and on Initialization it fired zero times on both paths, because the consent check blocks the tag and GTM does not release it when consent arrives later. Keep both.
Step 5: Fix the Reject all bug
This is the non-obvious part. The banner tells the visitor Necessary cookies stay on. The consent signals say otherwise the moment that visitor revokes. Until Framer reconciles the two, you fix it in GTM.
The tag below re-grants functionality_storage and security_storage on every cookie_consent_update. We watched it work on the live site: Framer denies both, and this tag grants them back in the next call. It is load-bearing, not a nicety. Remove it and a visitor who revokes consent loses basic site functionality that your own banner promised them.
Create a new tag in GTM:
Type: Custom HTML
Name:
Consent Fix — Functionality + Security Always GrantedHTML:
Trigger:
cookie_consent_update
This tag fires every time consent is updated (including when the user rejects everything) and forces the two necessary signals back to granted. It runs silently after Framer’s consent update and aligns the runtime signal with what the banner UI already showed the user.
Without this fix, basic site functionality can break for users who click Reject all, even though your banner promised them Necessary cookies are always active.
The fix repairs the click, not the next page load
The Consent Fix tag fires on cookie_consent_update, so it repairs the moment of the revoke. It does not repair the visit after that. Framer writes necessary: false into stored consent and leaves it there, and on the next page load nothing has changed, so no event fires and this tag never runs.
Something else has to cover the page loads. We tested both configurations on September 6, 2026 with necessary: false sitting in storage, using a restore tag that did not yet grant the two signals itself. The component behavior underneath, Framer’s own code and all twenty-six measured states, is in Framer’s Cookie Banner Denies the Cookies It Calls Always Active.
Banner alone, no head snippet. Framer’s consent default arrived first, carrying functionality_storage: denied and security_storage: denied. Nothing overrode it. Both signals stayed denied for that page load and every page load after it, and the banner does not reappear to give the visitor a way back.
Head snippet plus banner. The head snippet granted both signals before the container loaded. Framer’s denial arrived as a second consent default after the container had initialized, and gtag discarded it. Both stayed granted. The effective state was analytics_storage denied as the visitor asked, and the two necessary signals granted as the banner promised.
That result, measured across twenty-six states, is why the Step 3 tag now grants the two signals itself. Those two lines let the container cover the page loads on its own, rather than depending on the head snippet being present and on an event that a full rejection never fires.
The quirk behind Problem 1 is the same quirk that saves you in the second case. A late default is ignored, which is why you replay the visitor’s real choice yourself in Step 3, and why Framer’s necessary-denial never lands when the head snippet got there first.
Step 6: Configure your analytics and personalization tags
Hotjar and Bloomreach (or any analytics/personalization tool) should use the dual trigger pattern:
For each tag:
Add DOM Ready as a trigger
Add cookie_consent_update as a second trigger
In the tag settings, enable Additional Consent Checks (under Advanced Settings → Consent Settings) and require
analytics_storage
The consent check gates the tag. Even if the trigger fires, the tag won’t execute unless analytics_storage is granted.
The two triggers handle the two visitor states:
DOM Ready: returning visitor, stored consent already loaded by DOM Ready
cookie_consent_update: new visitor who just accepted, or returning visitor where Framer pushes consent during page load
GA4 / Google Tag does not need consent gating in the same way. Set it to fire on All Pages. GA4 with Consent Mode v2 handles its own consent signals and sends cookieless pings when consent is denied.
Step 7: Final GTM structure
Your Tags should look like this:
Tag | Triggers |
|---|---|
Consent Restore — Framer Stored Consent | Consent Initialization - All Pages |
Google Tag (GA4) | All Pages |
Hotjar Tracking Code | cookie_consent_update + DOM Ready |
Bloomreach / Exponea Tag | cookie_consent_update + DOM Ready |
Consent Fix tag | cookie_consent_update |
Your Triggers should be:
Consent Initialization - All Pages (built in)
All Pages (Page View)
DOM Ready
cookie_consent_update (Custom Event)
Step 8: Verify in GTM Preview mode
Open GTM → Preview → enter your site URL
Test as a new visitor (open in incognito):
On page load: Hotjar and Bloomreach should show as blocked by consent
After clicking Accept all:
cookie_consent_updatefires → both tags loadAfter clicking Reject all: both tags stay blocked, Consent Fix tag fires and grants functionality + security
Test as a returning visitor who previously accepted:
On Consent Initialization: the Consent Restore tag fires and
analytics_storageflips to grantedBoth gated tags then fire without waiting for user interaction
This is the case that fails silently. If you skip Step 3, the tags stay blocked and nothing in the interface says so
Check the consent timeline in GTM Preview, you should see:
Consent Initialization → all denied, then the restored update for a returning visitor
cookie_consent_update or DOM Ready → tags released
Tags fire after consent is confirmed
Count the container loads in the console with
document.querySelectorAll('script[src*="gtm.js"]').length. Two is correct when the head snippet is in place. Zero means neither loader ran. One with no head snippet means the banner is carrying GTM on its own, and your tags start several hundred milliseconds later than they need to.
Step 9: Update your Privacy Policy
Add a Third-Party Services section to your Privacy Policy listing each tool, what it collects, when it activates (after consent), and a link to the tool’s own privacy policy. At minimum cover:
Google Tag Manager
Hotjar
Bloomreach (if used)
Cloudflare (infrastructure, no consent required)
Why this matters
The GDPR requirement is not just that you show a banner, it is that tools do not process personal data before consent is given. A banner that looks compliant but loads tracking scripts on page load does not technically enforce consent, regardless of how it looks to the user.
The setup in this guide ensures:
No tracking fires before consent is known
Returning visitors keep the choice they already made, and are tracked from Consent Initialization rather than not at all
Returning visitors are tracked immediately without unnecessary blocking
Reject all genuinely blocks all tracking
Necessary functionality works regardless of consent choice
GA4 operates in cookieless mode when consent is denied, satisfying Google’s Consent Mode v2 requirement
Framer-specific notes
Framer pushes
cookie_consent_updateto the dataLayer whenever consent changes: this is the key event your GTM setup relies on. It does not push it when it replays a stored choice on a return visit, which is why Step 3 pushes the event itself. Verify both paths in DevTools → Console withwindow.dataLayerbefore relying on the event nameThe
functionality_storage/security_storageinconsistency described in Step 5 was still present on September 6, 2026, when we measured it across twenty-six states with every fix removed. Test your own version by opening DevTools, clicking Reject all, and inspecting the most recentconsententry in the dataLayerIf Framer aligns the signal with its banner UI in a future update, the Consent Fix tag does no harm: it just redundantly confirms what Framer already set correctly
As a Bloomreach agency working on Framer sites, we wire this exact consent stack for clients.
Read next
Once your consent setup is working, the Bloomreach manuals show how to wire each BR component into it correctly:
Loading Bloomreach Marketing (Engagement) Outside GTM: how to load the BR SDK in parallel with the consent stack this manual sets up
Fix the Bloomreach Marketing (Engagement) SDK Double-Load Warning in GTM: the Custom Template + Consent Mode queue double-load gotcha
The Bloomreach Marketing (Engagement) Console Error You Probably Don’t Know You Have: the
window.garetry exception on GA4-only sitesFramer’s Cookie Banner Denies the Cookies It Calls Always Active: the component behavior behind Step 5, measured across twenty-six states with every fix removed
WRITTEN BY

Jan Sacha
Co-Founder · Bloomreach Consultant
Bloomreach Marketing (Engagement) consultant, in his third year at Asteroad running daily CDP and marketing automation work for European clients. Former Exponea enterprise consultant and CEO of Digiline.
Rather have us set it up for you?
Every manual here comes from real implementation work. If you would rather hand it off, tell us what you are trying to set up. We will listen first. No pitch, no commitment.


