PHPackages                             goldnead/statamic-consent - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. goldnead/statamic-consent

ActiveStatamic-addon

goldnead/statamic-consent
=========================

Cookie consent banner and two-click embed gate for Statamic 6, editable in the control panel.

v1.4.1(today)057↑2636.8%proprietaryPHPPHP ^8.2CI passing

Since Aug 23Pushed todayCompare

[ Source](https://github.com/goldnead/statamic-consent)[ Packagist](https://packagist.org/packages/goldnead/statamic-consent)[ RSS](/packages/goldnead-statamic-consent/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (9)Used By (0)

Statamic Consent
================

[](#statamic-consent)

> Cookie banner and two-click embed gate, editable in the control panel.

Built for sites where the same person maintains the site and answers for it: the wording lives in a global set the client can edit, the handles live in the config file the developer controls, and a blocked embed is genuinely absent from the page rather than hidden with CSS.

[![The banner and a blocked embed](docs/banner.png)](docs/banner.png)

[![The services list in the control panel](docs/cp-services.png)](docs/cp-services.png)

Requirements
------------

[](#requirements)

Statamic 6 · PHP 8.2+ · no queue, no database, no build step.

Installation
------------

[](#installation)

```
composer require goldnead/statamic-consent
php please consent:install
```

(Under `php artisan` the same command is `statamic:consent:install`; `please` drops the prefix.)

The install command publishes the assets to `public/vendor/statamic-consent`, publishes the blueprint to `resources/blueprints/globals/consent.yaml`, and creates the **Consent** global set. It then appears in the control panel under **Globals → Consent**.

**Nothing renders until you add a service.** No services ship, so a fresh install leaves the site exactly as it was — see "When there is nothing to ask" below. Add the ones this site actually loads, in the control panel or in the config file.

**Re-run `php please consent:install` after every update.** The assets are overwritten on purpose: a half-updated pair of `consent.js` and `consent.css` behaves like the previous release.

Usage
-----

[](#usage)

In your layout:

```

    {{ consent:head }}

    ...
    {{ consent:settings_link }}
    {{ consent:banner }}

```

`{{ consent:settings_link }}` belongs on every page. A decision that cannot be revisited is not a decision that was freely given.

### Blocking an embed

[](#blocking-an-embed)

```
{{ consent:gate service="youtube" title="Konzertmitschnitt" cover="/img/cover.jpg" }}

{{ /consent:gate }}
```

The iframe is parked in a ``. Browsers parse a template but issue no requests for what is inside it, so nothing reaches YouTube until the visitor presses the button. Rendering the iframe and hiding it with CSS would look identical and be exactly the violation this tag exists to prevent.

A gate naming a service that is not configured stays blocked and says so, rather than falling through — a typo must not publish an unconsented embed.

[![The banner on a narrow screen](docs/mobile.png)](docs/mobile.png)

### Blocking a script

[](#blocking-a-script)

```

```

Parked scripts are re-created as real script elements once the service is allowed. Setting `.type` on the existing node would do nothing; the browser decided how to treat it at parse time.

### Tags

[](#tags)

TagParametersWhat it does`{{ consent:head }}`—Stylesheet, inline configuration, script. Belongs in ``.`{{ consent:banner }}`—The banner and the settings dialog. Belongs before ``.`{{ consent:gate }}``service` (required), `title`, `cover`Two-click gate around an embed.`{{ consent:granted }}``service` (required)Renders its contents only with consent, server-side.`{{ consent:settings_link }}``label`, `class`A button that reopens the dialog.`{{ consent:granted }}` reads the cookie on the server, so it is wrong on a page served from a full-page cache. Use the gate for anything that loads a third party; use `granted` for prose.

The cookie is written by JavaScript and is therefore not encrypted, so the addon registers it with `EncryptCookies::except()` on boot. Without that Laravel discards it and the server sees no cookie at all — which looks exactly like "nobody has consented yet". If you rename the cookie in a published config, the exemption follows the new name.

[![The settings panel](docs/dialog.png)](docs/dialog.png)

Configuration
-------------

[](#configuration)

`config/statamic-consent.php` owns the handles, the cookie and the behaviour. The **Consent** global set owns the wording and overrides the config key by key — a field left empty falls back to the shipped text in the visitor's language rather than rendering blank.

A **list** emptied in the control panel stays empty. Deleting every service is an answer, not a missing value, and the addon does not hand back what someone removed.

KeyDefaultWhat happens when it is wrong`cookie.name``statamic_consent`Renaming it after launch discards every stored decision; every visitor is asked again.`cookie.days``182`Longer than 12 months is not defensible under the GDPR.`cookie.same_site``Lax``None` without `Secure` makes browsers drop the cookie entirely.`version``1`Raise it when you add a non-essential service. Leaving it lets an old yes cover something the visitor never saw.`reject_on_dismiss``true`Setting it false treats a closed banner as consent, which it is not.`respect_gpc``true`Honours the Global Privacy Control signal, which German courts have read as a valid objection.`assets.styles``true`Off means you ship your own CSS against the class names below.`assets.scripts``true`Off disables the addon entirely; nothing unlocks.`categories`fourA category with no services is dropped from the dialog.`services`**none**Nothing ships. A service listed here appears in the banner, so an unused one describes data processing that does not happen. The `handle` is what templates refer to; renaming one after launch breaks every gate that names it.### Google Consent Mode v2

[](#google-consent-mode-v2)

Off by default. Switch it on only where the site actually loads gtag — an addon that creates a Google object on a site with no Google is the opposite of what it is for.

```
'google_consent_mode' => [
    'enabled' => true,
    'signals' => [
        'analytics_storage' => ['google_analytics'],
        'ad_storage' => ['google_ads'],
        'ad_user_data' => ['google_ads'],
        'ad_personalization' => ['google_ads'],
    ],
    'wait_for_update' => 500,
],
```

`{{ consent:head }}` then writes the `consent default` call **inline and first**, before anything else in the head, because Google's default has to be in place before any Google script loads. The runtime sends `consent update` as soon as the visitor decides, and again on every later change.

A signal is granted only when **every** service mapped to it is granted. A signal with an empty list stays denied — which is the right answer for anything you have not thought about yet.

### Styling

[](#styling)

The banner is a card in the bottom-left corner and the settings panel sits in the opposite corner, which is the shape adriangoldner.com established. **No font is loaded**: the banner inherits the site's own faces, which is why it reads as part of the page rather than a widget dropped onto it. Point the three type tokens at your theme's faces if you use utility classes rather than inherited typography:

```
:root {
    --csnt-font: 'Inter', system-ui, sans-serif;         /* body */
    --csnt-font-display: 'Outfit', system-ui, sans-serif; /* headings */
    --csnt-font-ui: 'JetBrains Mono', ui-monospace, monospace; /* buttons, captions */
}
```

Everything else is a token too. The full set, with the shipped defaults:

```
:root {
    --csnt-surface: #FFFFFF;          /* card and panel */
    --csnt-surface-sunken: #FAF8F4;   /* panel footer, blocked embed */
    --csnt-surface-muted: #F3F0EA;    /* the secondary button */
    --csnt-surface-muted-line: #EAE6DE;
    --csnt-ink: #141210;
    --csnt-ink-soft: #44403C;
    --csnt-muted: #78716C;            /* body copy, captions */
    --csnt-faint: #A8A29E;
    --csnt-line: rgba(20, 18, 16, 0.10);

    --csnt-brand: #E8B931;            /* the one accent */
    --csnt-brand-hover: #F5D04A;
    --csnt-brand-ink: #141210;        /* text on the accent */
    --csnt-brand-deep: #A67D14;       /* links, hover */

    --csnt-radius-card: 2.5rem;
    --csnt-radius-panel: 1.5rem;
    --csnt-shadow-card: 0 32px 64px -24px rgba(20, 18, 16, 0.14);
    --csnt-shadow-panel: 0 32px 64px -24px rgba(20, 18, 16, 0.18);
}
```

**Dark mode is opt-in.** Set `data-consent-theme="dark"` on `` for always dark, or `"auto"` to follow the operating system. Without it the banner stays light, because a widget that follows `prefers-color-scheme` on its own puts a dark dialog on a light site.

[![The dialog in a dark theme](docs/dialog-dark.png)](docs/dialog-dark.png)

To replace the stylesheet entirely, set `assets.styles` to `false` and write your own CSS against the class names (`csnt-banner`, `csnt-panel`, `csnt-gate`, `csnt-btn`, `csnt-pill`, `csnt-switch`, `csnt-caption`). **The class names and the published view paths (`resources/views/vendor/statamic-consent/`) are public API**; the CSS rules are not.

### JavaScript

[](#javascript)

```
StatamicConsent.granted('youtube')   // boolean
StatamicConsent.open()               // open the dialog
StatamicConsent.rejectAll()
StatamicConsent.reset()              // forget the decision, show the banner
document.addEventListener('consent:changed', e => e.detail.granted)
```

When there is nothing to ask
----------------------------

[](#when-there-is-nothing-to-ask)

If no optional service is configured — only essential ones, or none at all — `{{ consent:head }}`, `{{ consent:banner }}` and `{{ consent:settings_link }}` render **nothing**. No stylesheet, no script, no banner.

Strictly necessary cookies need no consent, so a site that loads no third party has nothing to put in a banner. Asking anyway trains people to click the nearest button, which is the opposite of an informed decision. It is also the state every installation is in on its first day: install the addon, and the site is unchanged until you enter a service.

A `{{ consent:gate }}` still blocks in that state. Failing open is never the safe answer.

Multi-site
----------

[](#multi-site)

**The wording is per site; the handles are shared.** The global set is localisable, so each site has its own banner text and service names, and a site without its own localisation falls back to the default site. Service handles come from the config file and are therefore identical everywhere — a `{{ consent:gate service="youtube" }}` in a shared layout behaves the same on every site.

The consent cookie is per domain, as browsers define it. Sites on separate domains ask separately; sites on paths of one domain share one decision.

Proof of consent
----------------

[](#proof-of-consent)

Article 7(1) GDPR puts the burden of proof on you, and a value in the visitor's own browser is not proof: it belongs to them and they can change it. Switch on a server-side record:

```
'record' => [
    'enabled' => true,
    'keep_days' => 400,
    'rate_limit' => 30,
],
```

Then `php artisan migrate`. The migration only loads while the record is on, so a flat installation that never wants one never gets a table.

**What is stored:** the random id from the cookie, a timestamp, the version consented to, the granted handles, how the decision was made, and the site. **Deliberately not stored: IP address and user agent.** Both are personal data in their own right and neither is needed — the id does the linking. Storing them turns a proof log into a visitor database.

Look one up when someone asks:

```
php please consent:lookup 94a5dd75-f45a-4775-a061-7b17bfc81224
php please consent:lookup --latest=50
php please consent:lookup --csv=nachweis.csv 94a5dd75-…
php please consent:prune              # deletes past keep_days
```

Two things worth understanding. The endpoint takes **no input**: the browser pings, and the server records what its own cookie says. Nothing in the request can be forged because nothing in it is read — and a cross-site post arrives without the cookie, because `SameSite=Lax` does not send one. That is also why it needs no CSRF token, which could not work on a cached page anyway.

And the record is a **side effect, never a step in the visitor's way**. If the write fails, the visitor has still decided.

There is no control panel screen for this. A native listing in Statamic 6 means a Vue build, a committed bundle and a CI job proving the bundle is current — machinery this addon deliberately does not carry. For a lookup that happens when a lawyer writes, a command is the better trade.

What it stores
--------------

[](#what-it-stores)

A first-party cookie (`statamic_consent`, six months) and a localStorage mirror, holding the granted handles, a timestamp, how the decision was made and a random id. **Nothing leaves your server** — the addon has no telemetry and no phone-home. The proof log above is off unless you switch it on, and it writes to your own database.

Uninstalling
------------

[](#uninstalling)

```
composer remove goldnead/statamic-consent
rm -rf public/vendor/statamic-consent resources/views/vendor/statamic-consent
rm resources/blueprints/globals/consent.yaml config/statamic-consent.php
rm -rf content/globals/consent.yaml content/globals/*/consent.yaml
```

Then remove the tags from your layout. Visitors keep a stale `statamic_consent` cookie until it expires; it is inert.

What this addon does not do
---------------------------

[](#what-this-addon-does-not-do)

It is not a legal opinion, and it does not discover what your site loads. Every third party has to be entered as a service, and an embed only gets blocked where a gate is put around it. It does not scan your theme to check that you did.

Tests
-----

[](#tests)

`composer test` runs the PHP suite. `npm test` runs the browser checks — the localStorage mirror, Global Privacy Control and parked scripts live only in the browser, so they are exercised against a fixture page rather than left to a reading of the source. It needs a Chrome on the machine (`CHROME_PATH` if it is not at `/usr/bin/google-chrome`); no browser is downloaded.

Support
-------

[](#support)

Only the latest version is supported. Issues:

Changelog · License
-------------------

[](#changelog--license)

[CHANGELOG.md](CHANGELOG.md) · [LICENSE.md](LICENSE.md)

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance100

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

8

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/85572d690277234a86834808cab169c4900922b4855fe9028426f8350dd74e97?d=identicon)[goldnead](/maintainers/goldnead)

---

Top Contributors

[![goldnead](https://avatars.githubusercontent.com/u/1313348?v=4)](https://github.com/goldnead "goldnead (17 commits)")

---

Tags

statamicgdprconsentdsgvocookie-bannerStatamic addon

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/goldnead-statamic-consent/health.svg)

```
[![Health](https://phpackages.com/badges/goldnead-statamic-consent/health.svg)](https://phpackages.com/packages/goldnead-statamic-consent)
```

###  Alternatives

[statamic/statamic

Statamic

832182.1k](/packages/statamic-statamic)[statamic-rad-pack/runway

Eloquently manage your database models in Statamic.

137236.2k8](/packages/statamic-rad-pack-runway)[statikbe/laravel-cookie-consent

Cookie consent modal for EU

219434.5k](/packages/statikbe-laravel-cookie-consent)[statamic/seo-pro

71548.3k](/packages/statamic-seo-pro)[duncanmcclean/statamic-cargo

Comprehensive e-commerce addon for Statamic. Build bespoke e-commerce sites without the complexity.

3622.8k](/packages/duncanmcclean-statamic-cargo)[jfeltkamp/cookiesjsr

COOKiES JSR is a consent management tool and provides UI and API to manage 3rd party services within the GDPR.

27902.9k](/packages/jfeltkamp-cookiesjsr)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
