Skip to article
YunaData← Journal
THE ADVENTURE JOURNAL
← Back to journalOpen page ↗ (opens in a new tab)

ADVENTURE JOURNAL ANALYTICS & SEO

Tutorial: A Website-Owned Cookie Banner with GTM, GA4 & Clarity

Pastel pixel-art computer with a shield, two optional settings switches and equal-sized buttons, beside star-shaped cookies and a sunny window.
In this entry

My cookie banner has a new home: the website’s own code. Visitors can open the controls and make a choice before Google Tag Manager loads. This is the setup I now use on YunaData, with Google Analytics 4, Microsoft Clarity and separately controlled Looker Studio reports.

My earlier tutorial records a custom banner delivered through GTM. This guide replaces that implementation. GTM-based consent solutions are still supported; Google documents consent templates for that architecture. Here, I wanted the privacy controls to work independently of the tag container.

This is a technical walkthrough of my implementation, not a certification of legal compliance. Your purposes, providers, disclosures and account settings need to agree with your code. For the design of the choices, Sweden’s PTS guidance describes active, informed consent, similarly accessible accept and reject options, choices by purpose, and easy withdrawal.

I use a blocking approach: the website does not insert the GTM script until analytics is allowed. GA4 and Clarity installation tags then run through that gated container. Google calls blocking Google tags until consent basic consent mode. Advanced mode can load tags and send measurements without cookies while storage consent is denied, so a denied flag alone does not mean that a tracker made no requests.

These are the choices in my banner:

  • Essential preferences: remember the visitor’s decision in this browser.
  • Usage analytics and session insights: GA4 measurement and Clarity heatmaps/session recordings. Off until selected.
  • Embedded reports: allow Looker Studio frames to load. This is a separate choice from analytics.

Advertising consent stays denied. Accept optional enables both optional choices; Reject optional disables both; Choose settings lets the visitor select either independently. Leaving the banner unanswered does not enable either category.

The flow is: read a saved choice → collect a choice if needed → queue the analytics consent update → load GTM once → send yd_analytics_ready → run the intended initialization tags. The report frames follow their own permission.

You need access to your HTML files and GTM container. My implementation uses three files. You can inspect them or download copies to adapt for your own site:

  • consent.js — state, storage, consent signals and the gated GTM loader.
  • consent-ui.js — the banner, settings dialog, reopen buttons and report controls.
  • consent.css — the banner’s styles and focus indicators.

These links point to this site’s current files. For your copy, set your own exact production hostname and GTM container ID near the top of consent.js. Use your real values in place of these examples:

JavaScript · configuration

My production allowlist contains only yunadata.pages.dev. Localhost and private or branch previews cannot start production analytics. Include any additional real production hostname explicitly; avoid broad suffix matches.

Adapt the provider descriptions in consent-ui.js, the privacy-policy page and the visual styling to your site. The policy URL is resolved beside consent.js; if you move these files into a different directory, update that URL. Keep the YunaConsent API name unless you also update every caller and GTM variable.

Add the following to each page’s <head>, before any analytics loader:

HTML · head

The core script is synchronous: do not add async or defer to it. Remove the old unconditional GTM loader, its noscript iframe, and any direct GA4 or Clarity installation that would bypass this loader. Also check theme/plugin injections if your site uses a CMS. One banner cannot stop a separate tracker installed elsewhere.

The script queues denied defaults first, then a valid stored or newly selected choice, before loading GTM. This follows the ordering described in Google’s consent implementation guide. The supplied file runs in the page; do not also paste it into a GTM Custom HTML tag. If you build consent management inside GTM instead, use its consent template APIs.

Put a real button in the footer of every page, alongside your privacy-policy link:

HTML · footer

The UI script reveals and binds the button. My room windows also contain a settings button, so visitors can reach it while reading an article in a dialog. The controls use native buttons, labelled checkboxes and a dialog with focus handling. Test them with a keyboard and on a small screen after styling them.

This implementation remembers a valid choice for up to 180 days. That is my chosen expiry, not a universal legal deadline. Invalid or expired records are ignored. If persistent storage is unavailable, the current tab can still honour the choice; the UI explains that it could not save it for future visits.

The steps below describe the intended configuration for this code. My Cloudflare migration has already been set up and tested; these instructions are for readers adapting the implementation, rather than a request to duplicate an existing working setup.

Retire the previous banner logic and banner CSS tags so they cannot inject a second banner or overwrite the new state. My GitHub Pages site is now retired and those old banner tags are paused. There is no longer a need to maintain its old banner branch.

In GTM, enable the built-in Page Hostname variable. Create a Custom JavaScript variable named JS - Yuna analytics allowed. Replace example.com with the same exact hostname you allowed in the core script:

JavaScript · GTM variable

Create a Custom Event trigger for yd_analytics_ready, with regex matching off. Choose Some Custom Events and put both conditions in this one trigger: Page Hostname equals your production hostname and JS - Yuna analytics allowed equals true.

TagTrigger for this implementationConsent check
Google tag / GA4 initializationyd_analytics_ready, with both guards; once per pageRequire analytics_storage
Clarity installationyd_analytics_ready, with both guards; once per pageRequire analytics_storage
GA4 interaction and game eventsKeep each existing event trigger and its conditions; add both guardsRequire analytics_storage

For the initialization tags, replace the old All Pages, Initialization or legacy cookie_consent_update firing triggers with the guarded ready trigger. Adding another firing trigger does not restrict the old one: firing triggers are alternatives. Keep the event names and parameters on gameplay tags unchanged.

Under Advanced Settings → Consent Settings, this implementation uses Require additional consent for tag to fire → analytics_storage. Google explains how additional checks block tags until their required types are granted. Review setup/cleanup tag sequencing too, so it cannot introduce an old banner or an extra tracker.

Use your own existing GA4 measurement and Clarity project IDs. Avoid duplicate initializers for the same destination. On my games, Firebase Analytics has its own consent-aware integration, so I keep that separate from the GTM event mapping; the details are in the game tracking notes. The ordinary Firestore leaderboard connection is separate from optional analytics.

The Google flags do not replace Clarity’s API. The supplied consent.js already queues this signal before Clarity can load when analytics is accepted:

JavaScript · already included

This is explanatory code, already included in the file; do not install it again as another tag. The capital S in both keys is intentional. Microsoft’s ConsentV2 documentation describes the API and explains that a loaded Clarity script can still operate with limited tracking when consent is denied. That is why this setup also gates its loader and unloads running scripts on withdrawal.

Review your actual Clarity project’s consent configuration, masking and retention settings. A page-level signal is one part of the setup; it does not verify those account settings for you.

A report iframe must start without a src; hiding an already loaded frame does not prevent its request. The UI script recognises the wrapper and attributes below. Replace the example report URL with your actual embed URL:

HTML · optional report

The report is enabled by the Embedded reports choice, even if analytics is declined. For content inserted after page load, call window.YunaConsent.enhance(container) after inserting it. This site does that when opening its room templates.

For ordinary portfolio interactions, use the supplied helper:

JavaScript · optional event

This helper discards events while analytics is unavailable or GTM is not ready. It does not record a backlog of pre-consent interactions to send later. Define only the events and non-sensitive parameters you actually need.

Saving a rejection sends denied signals, clears measurement cookies that the page can access, and refreshes the page if analytics had been loaded. Refreshing unloads already running tags; it also restarts a game in progress, which the game settings explain before the visitor saves. Declining before analytics starts does not require that refresh.

The choice is shared across tabs through storage events when persistent storage is available. An analytics withdrawal also stops tracking in other open tabs by refreshing tabs that had it running. This does not erase data already received by a provider, and JavaScript cannot clear every third-party or HttpOnly cookie.

Start on the real production hostname in a clean browser session without Tag Assistant. Open Developer Tools → Network and reload with the request list cleared. Inspect destinations and initiators, not just whether the banner looks correct. Ordinary hosting, image, font, selected data-feed and leaderboard requests are separate from optional analytics.

CaseExpected result with this implementation
First visit, before choosingNo GTM loader, GA4 or Clarity tracking; report frames stay unloaded.
Reject optional, then refreshThe rejection is restored and those optional services remain off.
Analytics onlyOne GTM loader and the intended measurement tags; report frames remain off; advertising remains denied.
Embedded reports onlyThe selected report can load; analytics remains off.
Returning visitor on a deep article URLThe saved choice is restored before analytics starts.
Withdraw analytics after acceptingThe page refreshes and returns with analytics off. Check another open tab too.
Storage or GTM unavailableControls and website content remain usable; tracking failure does not stop the page.
Keyboard and phoneBoth decisions and settings are reachable, text fits, and focus returns after closing settings.

Then use GTM Preview/Tag Assistant after accepting analytics. Confirm the denied defaults precede the grant, the ready event initializes each intended destination once, and the existing interaction events reach GA4. A blocked container before consent is expected in this design. Test withdrawal again outside the debugging session.

To repeat a first-visit test, remove only yunadata_consent_v1 from local storage and yunadata_consent_v1_session from session storage, then reload. Use Cookie settings to withdraw from an already running session; avoid clearing unrelated game or browser data.

During my Cloudflare migration checks, GTM tags fired and GA4 received events. After rejecting optional cookies and refreshing, my Network checks showed zero matching requests for Google Analytics, Microsoft Clarity and Ahrefs. That describes the checks I ran, rather than every possible browser state. The banner text in the linked files names GA4 and Clarity; the Ahrefs result was part of my rejection check. Review any additional container tag against your actual provider inventory, disclosures and consent gates before enabling it.

Whenever you add a tracking tool, embed or new purpose, review the banner text, privacy policy, consent gates and actual network requests together. Remember to publish any tested GTM changes; saving a workspace alone does not update its live container.

The current arrangement is easier for me to maintain because the controls live with the website, and the tag container receives an explicit decision before it starts measuring. You can also read the current Cloudflare GTM reference and the privacy policy.

Written on 24 October 2026. The linked Google, Microsoft and PTS documentation was checked for this update. The cover is an illustration of privacy choices, rather than a screenshot of the banner.