---
url: /frontend/consent.md
---
# Consent in the Theme

The consent state lives in two places: Google Consent Mode, which the GTM tags read, and the `evolve_consent` cookie, which the server and the theme scripts read. This page is the developer's view of the browser side. Policies, settings and the cookie table are on [Tracking and Consent](/site/tracking-and-consent).

## What the head partial defines

`theme::consentMode` (included by `theme::head`) runs before any tag and defines:

| Global | What it is |
|---|---|
| `window.EVOLVE_CONSENT` | `{ policy, cookieName, cookieLifetimeDays, banner, managedBy, state }`. `state` is `{ analytics, marketing }` from the cookie, or the policy default. `managedBy` is `cmp` when a consent platform is declared (`TRACKING_CONSENT_VIA_CMP` or `TRACKING_CMP`), otherwise `evolve`. |
| `window.evolveConsentAllows(category)` | `true` when `analytics` or `marketing` is granted right now. |
| `window.evolveConsentUpdate({ analytics, marketing })` | The one entry point for consent decided outside the Evolve banner. Since 2.19. |

Listen to the `evolve:consent-updated` window event to react to a change. Its `detail` is `{ analytics, marketing }`. Hotjar and the tracking config do this already.

## `window.evolveConsentUpdate`&#x20;

```js
window.evolveConsentUpdate({ analytics: true, marketing: false })
```

It does what the Evolve banner does when the visitor saves a choice:

1. writes the `evolve_consent` cookie in the banner's format: JSON `{ v: 1, analytics, marketing, ts }`, `max-age` from `cookieLifetimeDays`, `path=/`, `SameSite=Lax`. The server reads it on the next request.
2. sets `EVOLVE_CONSENT.state`.
3. calls `gtag('consent', 'update', ...)`, but only when `managedBy` is `evolve`. With a consent platform the platform updates Consent Mode itself, and a second update from Evolve could contradict it.
4. pushes `consent_updated` with `consent_analytics` and `consent_marketing` to the `dataLayer`.
5. dispatches `evolve:consent-updated`.

A choice equal to the stored cookie changes nothing and returns `false`, otherwise the call returns `true`. Consent platforms report the stored consent on every page load, so the guard keeps the cookie and the events quiet when nothing changed. Values are cast to booleans; a missing category counts as denied.

## Wiring a consent platform

**Cookiebot** needs no code: set the driver and the ID in the panel and Evolve renders `uc.js` and the listener (see [External consent platforms](/site/tracking-and-consent#external-consent-platforms)). Remove a project partial that loads Cookiebot itself once the driver is set, otherwise it loads twice.

**Any other platform** maps its categories to analytics and marketing and calls the bridge from its consent callback. Render the script after `theme::head`, so the bridge exists when the platform reports. A sketch for a platform with a JavaScript event:

```blade
@push('head')
<script data-cookieconsent="ignore">
    window.addEventListener('consent-platform:changed', function (event) {
        window.evolveConsentUpdate({
            analytics: event.detail.statistics === true,
            marketing: event.detail.marketing === true,
        });
    });
</script>
@endpush
```

Without such a call, `TRACKING_CONSENT_VIA_CMP` on an opt-in site only grants the GTM tags. Server-side conversions, Hotjar, the client tracking config and the `footprints` cookie stay denied, and the tracking health check warns.

## Project copies of the views

A project that copied `theme::head` or `theme::consentMode` (into `resources/views/vendor/theme/`) or renders its own consent snippet does not get the bridge. `php artisan evolve:view-overrides --since=2.18.0` lists the copies of the theme views and what they have to add. A project consent snippet that is not a copy of `theme::consentMode` is not detected; compare it with the package view by hand.
