PHPackages                             sandstorm/cookiepunch - 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. [Utility &amp; Helpers](/categories/utility)
4. /
5. sandstorm/cookiepunch

ActiveNeos-package[Utility &amp; Helpers](/categories/utility)

sandstorm/cookiepunch
=====================

Block elements like iframes and scripts before the markup reaches the browser and provide a dialog in the browser to enable them again.

5.1.2(1mo ago)759.2k↓33.2%3[1 issues](https://github.com/sandstorm/Sandstorm.CookiePunch/issues)MITCSS

Since Dec 5Pushed 1mo ago6 watchersCompare

[ Source](https://github.com/sandstorm/Sandstorm.CookiePunch)[ Packagist](https://packagist.org/packages/sandstorm/cookiepunch)[ Docs](https://github.com/sandstorm/Sandstorm.CookiePunch)[ RSS](/packages/sandstorm-cookiepunch/feed)WikiDiscussions main Synced today

READMEChangelog (10)Dependencies (2)Versions (42)Used By (0)

Sandstorm.CookiePunch
=====================

[](#sandstormcookiepunch)

A Neos package that blocks elements like `` and `` server-side — *before* the markup reaches the browser — and ships [Klaro](https://heyklaro.com/docs/) as the consent UI to selectively unblock them once the user agrees.

Contents
--------

[](#contents)

- [Features](#features)
- [How it works](#how-it-works)
- [Installation](#installation)
- [Minimal setup](#minimal-setup)
- [Basic Configuration and Usages](#basic-configuration-and-usages)
    - [Step 1: Adding the consent-modal](#step-1-adding-the-consent-modal)
    - [Step 2: Always allow your own JavaScript](#step-2-always-allow-your-own-javascript)
    - [Step 3: Blocking via YAML config](#step-3-blocking-via-yaml-config)
    - [Step 4: Providing a link to your privacy statement](#step-4-providing-a-link-to-your-privacy-statement)
    - [Step 5: Let the user reopen the consent modal later](#step-5-let-the-user-reopen-the-consent-modal-later)
    - [Step 6: Styling](#step-6-styling)
- [Advanced Usages](#advanced-usages)
    - [Full list of consent / service options](#full-list-of-consent--service-options)
    - [Supported tags](#supported-tags)
    - [Pattern reference](#pattern-reference)
    - [How blocking transforms markup](#how-blocking-transforms-markup)
    - [Blocking a rendered Fusion subtree](#blocking-a-rendered-fusion-subtree)
    - [Adding a contextual consent for non-iframe elements](#adding-a-contextual-consent-for-non-iframe-elements)
    - [Let the editor choose a service from the inspector](#let-the-editor-choose-a-service-from-the-inspector)
    - [Let the editor change the text of the consent](#let-the-editor-change-the-text-of-the-consent)
    - [Caching the consent](#caching-the-consent)
    - [Privacy URL alternatives](#privacy-url-alternatives)
    - [Manual styling](#manual-styling)
    - [Translations](#translations)
    - [Conditional Rendering of Services in the Consent Modal](#conditional-rendering-of-services-in-the-consent-modal)
    - [Editor-defined dynamic services](#editor-defined-dynamic-services)
    - [Per-service lifecycle callbacks (`onInit` / `onAccept` / `onDecline`)](#per-service-lifecycle-callbacks-oninit--onaccept--ondecline)
    - [Contextual Consent Only Mode](#contextual-consent-only-mode)
- [Troubleshooting](#troubleshooting)
- [Migration guide](./MIGRATIONS.md)
- [Contributing](./CONTRIBUTING.md)

Features
--------

[](#features)

- Eel helpers to block elements (scripts, iframes, and more) before the markup is sent to the client.
- Eel helper to place contextual consents anywhere in the markup.
- YAML configuration with patterns for targeting tags in the markup.
- Contextual-consent-only mode — no initial banner / modal.
- Localization via YAML and/or Fusion.
- Data source providing all services as a dropdown in the inspector.
- **A polished cookie-consent provided by [Klaro](https://heyklaro.com/docs/)**, bundled directly with this package:
    - Unblocking of elements after consent.
    - Contextual consents — temporarily or permanently unblock content from the element itself, without opening the modal.

How it works
------------

[](#how-it-works)

CookiePunch combines **server-side markup rewriting** with the **client-side Klaro consent UI**. On every render, the `CookiePunch.blockTags(...)` Eel helper walks the markup and breaks the configured tags: `src` becomes `data-src`, `type` becomes `data-type`, `` tags get `type="text/plain"`, and a `data-name=""` attribute is added when a service is in play. The browser refuses to fetch or execute the broken tags. Klaro then reads `data-name`, shows a consent UI, and on accept swaps the attributes back so the browser fetches and runs the original content.

The **service identifier** is the single string that ties (a) the YAML/inline blocking config, (b) the `data-name` in the rewritten markup, and (c) the switch in the Klaro modal together. Keep it consistent across all three and the rest follows.

```
# Data flow: server-side rewrite → browser → consent → restored markup
Fusion render
     │
     ▼  CookiePunch.blockTags / @process.blockTags
[rewritten markup]  …
     │
     ▼  shipped to browser
[Klaro JS]  reads data-name → renders switch → user consents
     │
     ▼  Klaro restores attributes
  ← browser fetches & runs

```

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

[](#installation)

```
# bash — from the application root
composer require sandstorm/cookiepunch
```

This puts the dependency in the outer `composer.json` / `composer.lock` (usually in your repo root or `/app`).

> **Important:** If you want to declare CookiePunch settings *inside one of your Flow packages*, also add the composer dependency to that package's `composer.json` to ensure correct Flow package and configuration loading order.

```
// DistributionPackages/Your.SitePackage/composer.json
{
    "require": {
        "sandstorm/cookiepunch": "*"
    }
}
```

For exhaustive references, see [`FullConsentConfig.yaml`](./Examples/Settings.CookiePunch.FullConsentConfig.yaml) and [`FullServiceConfig.yaml`](./Examples/Settings.CookiePunch.FullServiceConfig.yaml).

Minimal setup
-------------

[](#minimal-setup)

Two files get a working consent modal on every page. Drop them into your site package and reload:

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Neos.Neos:Page) {
    head.javascripts.cookiepunchConsent = Sandstorm.CookiePunch:Consent
    @process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend)}
}

```

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      purposes:
        essential:
          title: Essential
          description: Required for basic functionality.
      services: {}
    blocking:
      tagPatterns:
        script:
          # never block Neos' own scripts, or the backend breaks
          "Packages/Neos.Neos":
            block: false
```

This blocks every `` and `` (except Neos' own), shows the consent modal, and is enough to verify the install. The 6 Steps below layer in real services, pattern matching, and editor integrations. See also [`Examples/Settings.CookiePunch.Basic.yaml`](./Examples/Settings.CookiePunch.Basic.yaml).

Basic Configuration and Usages
------------------------------

[](#basic-configuration-and-usages)

### Step 1: Adding the consent-modal

[](#step-1-adding-the-consent-modal)

Drop a `CookiePunch.fusion` file in your site package:

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Neos.Neos:Page) {
    head.javascripts.cookiepunchConsent = Sandstorm.CookiePunch:Consent
    @process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend)}
}

```

This adds the consent modal and starts blocking every `` and ``. The `!node.context.inBackend` flag keeps the Neos backend functional.

Reload the page — it will likely look broken. Open the DevTools console and call `klaro.show()` to confirm Klaro is loaded; the next steps fix the breakage. For the full list of tag names you can pass to `blockTags`, see [Supported tags](#supported-tags).

### Step 2: Always allow your own JavaScript

[](#step-2-always-allow-your-own-javascript)

Some scripts (`main.js`, `app.js`, …) must always be allowed or your site won't work. You most likely have a Fusion prototype that bundles them — something like `Vendor.Site:HeaderAssets` — and that's the natural place to attach `neverBlockTags`:

```
// Resources/Private/Fusion/Component/HeaderAssets.fusion
prototype(Vendor.Site:HeaderAssets) < prototype(Neos.Fusion:Component) {
    renderer = afx`

    `
    @process.neverBlockTags = ${CookiePunch.neverBlockTags(["script"], value)}
}

```

For a one-off script tag, attach the helper directly:

```
// Resources/Private/Fusion/YourComponent.fusion
renderer = afx`

`

```

The same effect can be achieved via a YAML pattern (Step 3), but the helper makes the intent — *I checked, this script is required* — explicit at the call site.

### Step 3: Blocking via YAML config

[](#step-3-blocking-via-yaml-config)

Create `Configuration/Settings.CookiePunch.yaml`. Tip: register the package's `schema.json` in your IDE for auto-completion.

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      purposes:
        mediaembeds:
          title: Media Embeds
          description: Some Description
      services:
        anchor:
          title: Anchor FM
          description: Podcast Player
          purposes:
            - mediaembeds
    blocking:
      tagPatterns:
        script:
          "Packages/Neos.Neos":
            block: false
          "Packages/Vendor.ExampleLibrary":
            block: false
        iframe:
          "https://anchor.fm":
            service: anchor
```

The config has two parts: **`consent`** drives the Klaro UI (purposes group services), while **`blocking`** matches tags by substring pattern and either lets them through (`block: false`), blocks them permanently (`block: true`), or attaches them to a service so the user can allow them via consent (`service: anchor`).

Given the config above, this **input markup**:

```

```

…is transformed into this **output markup**:

```

```

For substring-matching rules, the wildcard `*`, and the difference between `block: false` / `block: true` / `service: …`, see [Pattern reference](#pattern-reference). For the exact attribute rewrites done to a "broken" tag, see [How blocking transforms markup](#how-blocking-transforms-markup).

### Step 4: Providing a link to your privacy statement

[](#step-4-providing-a-link-to-your-privacy-statement)

The default URL is `/privacy`. Override it with a string for the simplest case:

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      privacyPolicyUrl: /imprint/privacy
```

For most projects you'll want editors to pick the privacy page from the inspector. Add a reference property on your Homepage NodeType:

```
# NodeTypes/Document/Homepage/Document.Homepage.yaml
"Vendor.Site:Document.Homepage":
  properties:
    privacyPolicyUrl:
      type: reference
      ui:
        label: "Privacy page"
        inspector:
          group: "settings"
          editorOptions:
            nodeTypes: ["Neos.Neos:Document"]
```

…and point CookiePunch at it. `site` is already the Homepage, so no `q(site).find(...)` is needed:

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Sandstorm.CookiePunch:Config) {
    consent.privacyPolicyUrl = ${q(site).property("privacyPolicyUrl")}
    consent.privacyPolicyUrl.@process.convert = Neos.Neos:ConvertUris
}

```

For other approaches (XLIFF translation key, dedicated PrivacyPage node type), see [Privacy URL alternatives](#privacy-url-alternatives).

### Step 5: Let the user reopen the consent modal later

[](#step-5-let-the-user-reopen-the-consent-modal-later)

Place a link in Neos (e.g. in your privacy statement) with `href="#open_cookie_punch_modal"`. A click handler picks it up and opens the modal — the browser does not reload because `event.preventDefault()` is called internally.

Alternatively call `klaro.show()` from your own JavaScript.

### Step 6: Styling

[](#step-6-styling)

Override the CSS variables Klaro exposes via YAML:

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      styling:
        font-family: "Work Sans, Helvetica Neue, Helvetica, Arial, sans-serif"
        green1: "#00aa00"
        border-radius: "0"
```

For the full variable list, see [`Examples/Settings.CookiePunch.Styling.yaml`](./Examples/Settings.CookiePunch.Styling.yaml). To replace Klaro's CSS entirely with your own, see [Manual styling](#manual-styling).

Advanced Usages
---------------

[](#advanced-usages)

### Full list of consent / service options

[](#full-list-of-consent--service-options)

- [annotated YAML of consent options](./Examples/Settings.CookiePunch.FullConsentConfig.yaml)
- [annotated YAML of service options](./Examples/Settings.CookiePunch.FullServiceConfig.yaml)

Most inline comments are copied directly from the [annotated `config.js`](https://github.com/kiprotect/klaro/blob/ec6e36934db10afdac0183721ddfbcb9c79e7dc3/dist/config.js) of Klaro for convenience.

### Supported tags

[](#supported-tags)

`CookiePunch.blockTags(...)` and `CookiePunch.neverBlockTags(...)` accept any of these tag names:

`iframe`, `script`, `audio`, `video`, `source`, `track`, `img`, `embed`, `input`.

The same set is allowed as keys under `Sandstorm.CookiePunch.blocking.tagPatterns` in YAML — see [`schema.json`](./schema.json).

### Pattern reference

[](#pattern-reference)

Patterns under `tagPatterns.` are matched against the raw rendered tag string with `strpos()` — i.e. it's a substring match. Anything in the tag (`src` URL, attribute name, attribute value, …) is fair game.

The `Packages/Neos.Neos` pattern, for example, matches all of these:

```

```

Each pattern carries one of three actions:

```
# Configuration/Settings.CookiePunch.yaml
"Packages/Neos.Neos":
  block: false        # always allowed
"https://really-stuff.bad":
  block: true         # always blocked, the consent cannot allow it
"https://anchor.fm":
  service: anchor     # blocked, but the user can allow via consent
```

#### Wildcard (`"*"`)

[](#wildcard-)

The reserved key `"*"` flips the *default* for a tag name. Use sparingly — it defeats the purpose of documenting which services are in use.

The most defensible case is ``: by default you usually do not want every image blocked, only specific tracking pixels. Add `img` to the `blockTags` call so CookiePunch processes it, then flip the default:

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Neos.Neos:Page) {
    head.javascripts.cookiepunchConsent = Sandstorm.CookiePunch:Consent
    @process.blockTags = ${CookiePunch.blockTags(["iframe","script", "img"], value, !node.context.inBackend)}
}

```

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    blocking:
      tagPatterns:
        img:
          "*":
            block: false
          "tracking-pixel-url":
            service: myservice
```

### How blocking transforms markup

[](#how-blocking-transforms-markup)

When CookiePunch breaks a tag, it does so by attribute rewriting — nothing is removed from the DOM:

- `src` → `data-src`, so the browser doesn't fetch the resource.
- For `` tags only: the original `type` is moved to `data-type` and the live attribute is replaced with `type="text/plain"`, so the browser refuses to execute the script.
- If the matching pattern points at a service, `data-name=""` is added. Klaro reads this attribute, presents a contextual consent, and on accept swaps the `data-*` attributes back to their live counterparts.

A tag with no `data-name` stays broken forever — there is no service to drive its restoration.

### Blocking a rendered Fusion subtree

[](#blocking-a-rendered-fusion-subtree)

An already-blocked piece of markup is *not* re-blocked when running the Eel helpers later on `Neos.Neos:Page`. This means we can hook into specific plugins to block them and attach them to a service.

This is especially useful for inline `...` tags that cannot be matched by a URL pattern.

```
// Resources/Private/Fusion/Plugin/FooTube.fusion — plugin implementation
prototype(Vendor.Plugin.FooTube:Embed) < prototype(Neos.Fusion:Component) {
    renderer = afx`

        ...

    `
}

```

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Vendor.Plugin.FooTube:Embed) {
  // tags in this part of the tree will be blocked first
  @process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend, "footube")}
}

prototype(Neos.Neos:Page) {
  head.javascripts.cookiepunchConsent = Sandstorm.CookiePunch:Consent
  // at last, all remaining tags will be blocked according to the config
  // already blocked tags will be ignored
  @process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend)}
}

```

### Adding a contextual consent for non-iframe elements

[](#adding-a-contextual-consent-for-non-iframe-elements)

When blocking a `` you may end up with a broken UI as some styles or markup never run. Use the helper below to wrap parts of the rendered Fusion tree so Klaro can swap the broken content for a contextual consent.

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Vendor.Plugin.FooTube:Embed) {
  @process.blockTags = ${CookiePunch.blockTags(["script"], value, !node.context.inBackend, "footube")}
  @process.addContextualConsent = ${CookiePunch.addContextualConsent("footube", value, !node.context.inBackend)}
}

```

Another use case: `` or `` tags (with or without nested `` tags). You may want to block them so a visitor's IP address isn't sent to a third-party server before consent.

```
// Resources/Private/Fusion/Component/ThirdpartyAudio.fusion
prototype(Vendor:Component.ThirdpartyAudio) < prototype(Neos.Fusion:Component) {
  thirdpartySrc = ''

  renderer = afx`

  `
  @process.blockTags = ${CookiePunch.blockTags(["source"], value, !node.context.inBackend, "thirdpartymedia")}
  @process.addContextualConsent = ${CookiePunch.addContextualConsent("thirdpartymedia", value, !node.context.inBackend)}
}

```

### Let the editor choose a service from the inspector

[](#let-the-editor-choose-a-service-from-the-inspector)

If editors can place HTML (e.g. via a `Vendor.Site:Content.Html` node type), they can introduce markup that sets cookies. With the default config, CookiePunch blocks this content — and if the markup matches no YAML pattern, it stays blocked permanently.

Add `Sandstorm.CookiePunch:Mixin.ConsentServices` to the affected node type to expose a service dropdown in the inspector:

```
# NodeTypes/Content/Html/Content.Html.yaml
"Vendor.Site:Content.Html":
  superTypes:
    "Sandstorm.CookiePunch:Mixin.ConsentServices": true
```

Then wire the chosen service into the actual blocking:

```
// NodeTypes/Content/Html/Content.Html.fusion
prototype(Vendor.Site:Content.Html) {
  @process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend, q(node).property("consentServices"))}
  // Wrap the html element with `...` to make sure
  // the contextual consent is displayed correctly
  @process.contextualConsent = ${CookiePunch.addContextualConsent(q(node).property("consentServices"), value, !node.context.inBackend)}
}

```

### Let the editor change the text of the consent

[](#let-the-editor-change-the-text-of-the-consent)

All texts of the consent notice and modal live in the Fusion prototype `Sandstorm.CookiePunch:Config.Translations`. Each key maps to a Klaro string — `ok`, `decline`, `consentNotice.description`, `consentNotice.learnMore`, `consentModal.title`, `consentModal.description`, `privacyPolicy.text`, `contextualConsent.*`, and more. The complete list is in [`Resources/Private/Fusion/Config.Translations.fusion`](./Resources/Private/Fusion/Config.Translations.fusion).

You can override any of these from Fusion. To let editors maintain them, wire the paths to inspector properties on a dedicated node.

#### 1. A node holding the editable texts

[](#1-a-node-holding-the-editable-texts)

```
# NodeTypes/Document/CookieConsentTexts/Document.CookieConsentTexts.yaml
"Vendor.Site:Document.CookieConsentTexts":
  superTypes:
    "Neos.Neos:Document": true
  ui:
    label: "Cookie consent texts"
    icon: icon-cookie
    inspector:
      groups:
        consent:
          label: "Cookie consent"
  properties:
    ok:
      type: string
      ui:
        label: "Accept button"
        inspector: { group: consent }
    decline:
      type: string
      ui:
        label: "Decline button"
        inspector: { group: consent }
    consentNoticeDescription:
      type: string
      ui:
        label: "Notice text (use {imprint} for the imprint link)"
        inspector:
          group: consent
          editor: Neos.Neos/Inspector/Editors/TextAreaEditor
    consentNoticeLearnMore:
      type: string
      ui:
        label: "Notice 'learn more' link"
        inspector: { group: consent }
    consentModalTitle:
      type: string
      ui:
        label: "Modal title"
        inspector: { group: consent }
    consentModalDescription:
      type: string
      ui:
        label: "Modal text"
        inspector:
          group: consent
          editor: Neos.Neos/Inspector/Editors/TextAreaEditor
    privacyPolicyText:
      type: string
      ui:
        label: "Privacy-policy line (use {imprint} for the imprint link)"
        inspector:
          group: consent
          editor: Neos.Neos/Inspector/Editors/TextAreaEditor
    imprint:
      type: reference
      ui:
        label: "Imprint page"
        inspector:
          group: consent
          editorOptions:
            nodeTypes: ["Neos.Neos:Document"]
```

#### 2. Wire the properties into `Config.Translations`

[](#2-wire-the-properties-into-configtranslations)

Use `|| CookiePunchConfig.translate(...)` for keys where an empty inspector field should fall back to the bundled Klaro translation instead of blanking the string:

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Sandstorm.CookiePunch:Config.Translations) {
    @context._texts = ${q(site).find('[instanceof Vendor.Site:Document.CookieConsentTexts]').get(0).properties}
    // Resolve the referenced imprint node to an  tag we can splice into the text
    @context._imprintLink = Neos.Neos:NodeLink {
        node = ${_texts.imprint}
    }

    ok = ${_texts.ok || CookiePunchConfig.translate("Sandstorm.CookiePunch.translations.ok")}
    decline = ${_texts.decline || CookiePunchConfig.translate("Sandstorm.CookiePunch.translations.decline")}

    consentNotice {
        description = ${String.replace(_texts.consentNoticeDescription, '{imprint}', _imprintLink)}
        learnMore = ${_texts.consentNoticeLearnMore || CookiePunchConfig.translate("Sandstorm.CookiePunch.translations.consentNotice.learnMore")}
    }
    consentModal {
        title = ${_texts.consentModalTitle}
        description = ${_texts.consentModalDescription}
    }
    privacyPolicy {
        text = ${String.replace(_texts.privacyPolicyText, '{imprint}', _imprintLink)}
    }

    // Keys you do not override fall back to the bundled Klaro translations automatically.
}

```

#### 3. Enable HTML rendering for the descriptions

[](#3-enable-html-rendering-for-the-descriptions)

`Neos.Neos:NodeLink` renders a full `…` tag, so any text containing the `{imprint}` substitution now contains HTML. Allow Klaro to render it:

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      # Renders the descriptions of the consent modal/notice as HTML. Use with care.
      htmlTexts: true
```

#### 4. Flush the cache when editors change the texts

[](#4-flush-the-cache-when-editors-change-the-texts)

The override reads from `q(site).find(...)` and is rendered inside the cached `Neos.Neos:Page`. Add `Neos.Caching.nodeTypeTag('Vendor.Site:Document.CookieConsentTexts')` to the consent cache — see [Caching the consent](#caching-the-consent).

#### Notes &amp; caveats

[](#notes--caveats)

- The full set of overridable paths is in [`Config.Translations.fusion`](./Resources/Private/Fusion/Config.Translations.fusion).
- An empty inspector property silently blanks the bundled default. Use the `|| CookiePunchConfig.translate(...)` fallback for any string where that would be a regression.
- `Neos.Neos:NodeLink` renders an `` tag — `htmlTexts: true` is mandatory wherever you substitute it in.
- The `@cache` entry tag on the texts node type is not optional; without it, edits don't propagate. See [Caching the consent](#caching-the-consent).
- For multi-language sites where the text varies by locale, XLIFF (see [Translations](#translations)) is the better tool. Inspector properties suit editor-owned wording, not translator-owned.

### Caching the consent

[](#caching-the-consent)

`Sandstorm.CookiePunch:Consent` ships **without** a `@cache` block. It is rendered inside the cached `Neos.Neos:Page`, so any dynamic read it makes — `q(site).find(...)` for conditional services, dynamic services, or editor-maintained texts — gets baked into each page's cache entry. Without explicit cache tags, editing the source nodes never reaches already-cached pages.

Override the prototype once with the canonical block and merge **all** the entry tags the rest of your setup needs:

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Sandstorm.CookiePunch:Consent) {
    @cache {
        mode = 'cached'
        entryIdentifier {
            node = ${node}
        }
        entryTags {
            1 = ${Neos.Caching.nodeTag(node)}
            // Add one numeric key per nodeTypeTag your dynamic reads depend on:
            // 2 = ${Neos.Caching.nodeTypeTag('Vendor.Site:Document.CookieConsentTexts')}      // editor-maintained consent texts
            // 3 = ${Neos.Caching.nodeTypeTag('Vendor.Site:Content.CookieConsentEmbed')}      // editor-defined dynamic services
            // 4 = ${Neos.Caching.nodeTypeTag('Vendor.Site:Document.RootPage')}                // when:-expressions reading site properties
            // 5 = ${Neos.Caching.nodeTypeTag('Vendor.Site:Content.YouTube')}                  // when:-expressions counting content nodes
        }
    }
}

```

**Why this lives in one place.** The `entryTags` keys must be unique within the block — if you copy the snippet from two Advanced sections that each define `entryTags { 1 = ...; 2 = ... }`, the later override silently wins and your first feature stops invalidating. Keep one `@cache` block in your project and add a new numbered tag for each Advanced feature you adopt.

### Privacy URL alternatives

[](#privacy-url-alternatives)

Beyond the simple-string and Homepage-property forms shown in [Step 4](#step-4-providing-a-link-to-your-privacy-statement), two other paths are available.

#### XLIFF translation key

[](#xliff-translation-key)

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      privacyPolicyUrl: Vendor.Site:Main:privacyPolicyUrl
```

#### Dedicated PrivacyPage node type

[](#dedicated-privacypage-node-type)

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Sandstorm.CookiePunch:Config) {
    consent.privacyPolicyUrl = Neos.Neos:NodeUri {
        node = ${q(site).find('[instanceof Vendor.Site:Document.PrivacyPage]').get(0)}
    }
}

```

### Manual styling

[](#manual-styling)

To take full control of the consent UI's CSS, disable the bundled stylesheet and provide your own. Note this couples your styling to Klaro's class names — it can break on package updates.

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Neos.Neos:Page) {
    head.javascripts.cookiepunchConsent = Sandstorm.CookiePunch:Consent {
        noCSS = true
    }
    @process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend)}
}

```

The original Klaro stylesheet ships at `Resources/Private/KlaroCss/klaro.css` if you want to fork from it.

### Translations

[](#translations)

Klaro already provides translations for many languages. They are exposed as XLIFF files in `Resources/Private/Translations`.

You can override translations by:

- creating your own XLIFF files that override the defaults,
- providing a translation key (e.g. `Vendor.Site:CookiePunch:services.youtube.description`) instead of literal text in the YAML config,
- overriding the corresponding path in the Fusion prototypes `Sandstorm.CookiePunch:Config.Translations` or `Sandstorm.CookiePunch:Config`.

**Example: translating service labels**

Service labels in your `Settings.CookiePunch.yaml` can be translated like this:

```
# Configuration/Settings.CookiePunch.yaml
services:
  youtube:
    title: Youtube
    description: Vendor.Site:CookiePunch:services.youtube.description
```

Where:

- `Vendor.Site` is your site package key,
- `CookiePunch` is the name of the XLIFF file containing the translations (any name — must match the file name). See screenshot:

[![Screenshot 2022-06-07 at 14 37 57](https://user-images.githubusercontent.com/9661367/172380821-9c374cb4-35ab-4892-afe3-f6cd09885981.png)](https://user-images.githubusercontent.com/9661367/172380821-9c374cb4-35ab-4892-afe3-f6cd09885981.png)

- and inside the file you reference the key after the colon (here: `services.youtube.description`):

```

    Erlaubt die Einbindung von Youtube-Videos.

```

### Conditional Rendering of Services in the Consent Modal

[](#conditional-rendering-of-services-in-the-consent-modal)

You can decide at runtime whether a switch should appear in the consent modal:

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      services:
        youtube:
          title: Youtube
          description: ...
          purposes:
            - mediaembeds
          when: "${q(site).find('[instanceof Vendor.Site:Content.YouTube]').count() > 0}"
        googleAnalytics:
          title: Google Analytics
          description: ...
          purposes:
            - analytics
          when: "${q(site).property('googleAnalyticsAccountKey')}"
```

For a complete example see [`Examples/Settings.CookiePunch.WithWhenConditions.yaml`](./Examples/Settings.CookiePunch.WithWhenConditions.yaml).

This is useful in multi-site setups, and to prevent unnecessary consent switches when e.g. no YouTube video has ever been added to the content (the `Vendor.Site:Content.YouTube` node type above stands in for whichever content type embeds a YouTube video in your site).

**Notes:**

1. The `when` value must be an Eel expression that evaluates to boolean.
2. With no `when` condition, the default is `${true}` — the switch always renders for that service.
3. When querying the content repository with `q(...)`, only `site` is available. `documentNode` and `node` are not.
4. Klaro stores past consent decisions in a cookie, so removing and re-adding e.g. a YouTube video will not re-prompt users who already consented.

**Important:** every node type referenced in your `when` expressions needs a matching `Neos.Caching.nodeTypeTag(...)` on the consent cache (e.g. `Vendor.Site:Document.RootPage` for `q(site).property(...)`, `Vendor.Site:Content.YouTube` for `q(site).find('[instanceof ...YouTube]').count()`). See [Caching the consent](#caching-the-consent) for the canonical block.

**Preventing an empty consent modal**

If all `when` expressions evaluate to false you can hide the modal entirely:

```
// Resources/Private/Fusion/CookiePunch.fusion
prototype(Sandstorm.CookiePunch:Consent) {
    // only render if there is at least one service that has not been filtered out by its 'when' config key
    @if.hasServices = ${Array.length(this.servicesRemainingAfterWhenConditions) > 0}
}

```

### Editor-defined dynamic services

[](#editor-defined-dynamic-services)

[Let the editor choose a service from the inspector](#let-the-editor-choose-a-service-from-the-inspector) lets editors pick from a **predefined** list of services. Sometimes you want them to **create a new service on the fly** — e.g. a content element where the editor pastes a third-party embed, names the service, and a matching switch appears in the consent automatically.

The trick is to override `Sandstorm.CookiePunch:Consent` and append dynamically-built services to `servicesRemainingAfterWhenConditions` (the same property used in [Conditional Rendering](#conditional-rendering-of-services-in-the-consent-modal)). The service key is derived by hashing the editor's typed name, so the same value can be used on both the blocking side and the consent side.

#### 1. A content element node type

[](#1-a-content-element-node-type)

```
# NodeTypes/Content/CookieConsentEmbed/Content.CookieConsentEmbed.yaml
"Vendor.Site:Content.CookieConsentEmbed":
  superTypes:
    "Neos.Neos:Content": true
  ui:
    label: "Third-party embed (with consent)"
    inspector:
      groups:
        consent:
          label: "Cookie consent"
  properties:
    serviceName:
      type: string
      validation:
        "Neos.Neos/Validation/NotEmptyValidator": []
      ui:
        label: "Service name (shown in the cookie consent)"
        inspector:
          group: consent
    serviceDescription:
      type: string
      ui:
        label: "Service description"
        inspector:
          group: consent
    embedCode:
      type: string
      ui:
        label: "Embed code (script / iframe)"
        reloadIfChanged: true
        inspector:
          group: consent
          editor: Neos.Neos/Inspector/Editors/CodeEditor
```

#### 2. Render and block the element's own markup

[](#2-render-and-block-the-elements-own-markup)

The element renders the embed, then blocks it and attaches the contextual consent. The service key is the md5 of the editor's `serviceName`:

```
// NodeTypes/Content/CookieConsentEmbed/Content.CookieConsentEmbed.fusion
prototype(Vendor.Site:Content.CookieConsentEmbed) < prototype(Neos.Neos:ContentComponent) {
    // derive the service key once; the consent override below MUST hash the same way
    @context.serviceKey = ${String.md5(q(node).property('serviceName'))}

    renderer = afx`
        {String.htmlSpecialCharsDecode(q(node).property('embedCode'))}
    `
    @process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend, serviceKey)}
    @process.addContextualConsent = ${CookiePunch.addContextualConsent(serviceKey, value, !node.context.inBackend)}
}

```

#### 3. Register a service for every embed

[](#3-register-a-service-for-every-embed)

Override the consent prototype to scan the site for these elements and append one service per distinct name:

```
// Resources/Private/Fusion/Overrides/CookiePunch.fusion
prototype(Sandstorm.CookiePunch:Consent) {
    // recompute the statically-configured services (we cannot self-reference
    // servicesRemainingAfterWhenConditions, so we rebuild it from the config)
    @context.originalServices = ${CookiePunchConfig.filterServicesArrayByWhenCondition(Configuration.setting("Sandstorm.CookiePunch.consent.services"), site)}

    @context.dynamicServices = Neos.Fusion:Map {
        items = ${q(site).find('[instanceof Vendor.Site:Content.CookieConsentEmbed][serviceName != ""]')}
        itemRenderer = Neos.Fusion:DataStructure {
            // NOTE: no `name` here — Config.fusion derives the Klaro service name
            // from the map KEY (keyRenderer), not from a `name` field.
            title = ${q(item).property('serviceName')}
            description = ${q(item).property('serviceDescription')}
            purposes = ${['externalContent']}
        }
        // The KEY becomes the Klaro service name. It MUST match the `data-name`
        // produced by the element's blockTags/addContextualConsent above —
        // i.e. hash `serviceName` exactly the same way. Using the hash as key
        // also deduplicates: two embeds with the same name share one switch
        // (the last one rendered wins for title/description).
        keyRenderer = ${String.md5(q(item).property('serviceName'))}
    }

    servicesRemainingAfterWhenConditions = ${Array.concat(originalServices, dynamicServices)}
}

```

Then add `Neos.Caching.nodeTypeTag('Vendor.Site:Content.CookieConsentEmbed')` to the consent cache so a newly published embed flushes every page's consent — see [Caching the consent](#caching-the-consent).

#### 4. Declare the purpose

[](#4-declare-the-purpose)

Every purpose a service references must exist under `consent.purposes` (for its title/description and translations):

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      purposes:
        externalContent:
          title: External content
          description: Embedded third-party content that may set cookies.
```

#### Notes &amp; caveats

[](#notes--caveats-1)

- **The two `String.md5(...)` expressions must stay byte-identical** (the element in step 2 and the consent override in step 3). If they ever drift, the markup is blocked but no service can unblock it — the content stays broken forever.
- **Deduplication is by name.** Two embeds with the same `serviceName` produce one switch; the last-rendered node wins for `title`/`description`.
- **Make `serviceName` required.** An empty name hashes to a constant (`md5('')`), collapsing unrelated embeds into one bogus service — hence the `NotEmptyValidator` and the `[serviceName != ""]` filter.
- **The `@cache` entry tag is not optional** — without it, new embeds don't appear on already-cached pages. See [Caching the consent](#caching-the-consent).

### Per-service lifecycle callbacks (`onInit` / `onAccept` / `onDecline`)

[](#per-service-lifecycle-callbacks-oninit--onaccept--ondecline)

Each service can declare JavaScript snippets that run when Klaro initialises, when the user accepts, and when the user declines:

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      services:
        googleAnalytics:
          title: Google Analytics
          purposes: [analytics]
          # JS executed when Klaro initialises the service
          onInit: "console.log('GA init');"
          # JS executed when the user gives consent
          onAccept: "window.dataLayer.push({'event': 'cookie_consent_ga'});"
          # JS executed when the user withdraws consent
          onDecline: "console.log('GA declined');"
```

Each value is the *body* of a JS function. The strings are exposed via `window.cookiePunchCallbacks` and registered with Klaro before the main bundle loads — so they work under strict CSP without `unsafe-eval`. (Prior to v5 these were registered via `eval()`. See [MIGRATIONS.md](./MIGRATIONS.md#migrating-from-version-4-to-5).)

A complete service config showing every supported key — including these callbacks — is in [`Examples/Settings.CookiePunch.FullServiceConfig.yaml`](./Examples/Settings.CookiePunch.FullServiceConfig.yaml).

### Contextual Consent Only Mode

[](#contextual-consent-only-mode)

If you don't want to show the cookie banner or modal initially, use the global `contextualConsentOnly` mode introduced with [version 4.4.0](https://github.com/sandstorm/Sandstorm.CookiePunch/releases/tag/4.4.0).

```
# Configuration/Settings.CookiePunch.yaml
Sandstorm:
  CookiePunch:
    consent:
      contextualConsentOnly: true
      mustConsent: false
```

Troubleshooting
---------------

[](#troubleshooting)

### The consent modal doesn't appear

[](#the-consent-modal-doesnt-appear)

**Please check**

- Is `Sandstorm.CookiePunch:Consent` actually included in your `Neos.Neos:Page` (e.g. `head.javascripts.cookiepunchConsent = Sandstorm.CookiePunch:Consent`)?
- Does `klaro.show()` work in the DevTools console?
- Does the browser console show CSP errors about an inline ``?

**This could be the problem**

- The Fusion include is missing or shadowed by another `head.javascripts.*` assignment.
- A strict Content Security Policy without `unsafe-inline` (or a matching nonce/hash) is blocking the inline `` produced by `Sandstorm.CookiePunch:Js.Config` and `Sandstorm.CookiePunch:Js.Callbacks`.

**How to fix**

Add the Fusion include from [Step 1](#step-1-adding-the-consent-modal). If CSP blocks the inline script, either allow `script-src 'unsafe-inline'` (not recommended) or attach a nonce/hash to the consent's script tag.

### A service switch is missing from the modal

[](#a-service-switch-is-missing-from-the-modal)

**Please check**

- Is the service declared under `Sandstorm.CookiePunch.consent.services` in your YAML?
- Does its `when:` expression (if any) evaluate truthy for the current `site`?
- For editor-defined dynamic services, is the page's `Sandstorm.CookiePunch:Consent` `@cache` tagged on the embed/text node type?

**This could be the problem**

- A YAML typo / mis-nesting under the wrong key (`Sandstorm:CookiePunch:` vs. `Sandstorm: { CookiePunch: { … } }`).
- A `when:` Eel expression is silently false (`q(site).find(...).count() == 0`, missing property).
- The page is showing a cached version that pre-dates the service's existence — see [Caching the consent](#caching-the-consent).

**How to fix**

Open the DevTools console and inspect `window.cookiePunchConfig` — every service the server emitted is listed there. If yours is missing, the issue is in the Fusion/YAML; if it's present but the switch isn't, see [Caching the consent](#caching-the-consent) and flush the page cache.

### Content stays blocked even after the user accepts

[](#content-stays-blocked-even-after-the-user-accepts)

**Please check**

- View the rendered HTML (not the live DOM). Does the broken tag have a `data-name="…"` attribute?
- Does the value of that `data-name` match a service `name` from `window.cookiePunchConfig.services`?

**This could be the problem**

- The matching pattern attached `block: true` (no service) instead of `service: someService`, so Klaro cannot restore the markup.
- For [Editor-defined dynamic services](#editor-defined-dynamic-services), the two `String.md5(...)` expressions (element side vs. consent side) have drifted — typically because the element-side `serviceName` was edited but the consent-side query didn't refresh.
- A pattern matched the URL but the wildcard `"*": false` is also present and won by mistake — see [Pattern reference](#pattern-reference).

**How to fix**

Make `data-name` and the service `name` byte-identical. For dynamic services, ensure the consent's `@cache` is tagged on the embed node type so updates propagate.

### The Neos backend looks broken

[](#the-neos-backend-looks-broken)

**Please check**

- Did you pass the `!node.context.inBackend` argument to `CookiePunch.blockTags(...)`?

**This could be the problem**

- `blockTags(["iframe","script"], value)` (only two arguments) blocks unconditionally — including in the Neos backend, which breaks the editor UI.

**How to fix**

Always pass `!node.context.inBackend` as the third argument:

```
// Resources/Private/Fusion/CookiePunch.fusion
@process.blockTags = ${CookiePunch.blockTags(["iframe","script"], value, !node.context.inBackend)}

```

### Edits to dynamic-source nodes don't show up on already-published pages

[](#edits-to-dynamic-source-nodes-dont-show-up-on-already-published-pages)

**Please check**

- Does your `Sandstorm.CookiePunch:Consent` override declare a `@cache` block with a `nodeTypeTag` for the source node type?

**This could be the problem**

- The consent is rendered inside `Neos.Neos:Page`. Without explicit cache tags, `q(site).find(...)` reads are frozen into each page's cache entry.

**How to fix**

Add the missing `Neos.Caching.nodeTypeTag('Vendor.Site:Document.X')` to the consent cache — see [Caching the consent](#caching-the-consent).

### Iframes work after unblocking but are the wrong size or in the wrong place

[](#iframes-work-after-unblocking-but-are-the-wrong-size-or-in-the-wrong-place)

**Please check**

- Do you have an iframe that you blocked because it sets cookies?
- Do you have JS that manipulates this iframe?
- Is the JS *not* blocked while the iframe *is*?
- Does a reload after consenting fix the problem?

**This could be the problem**

- The JS runs once on page load, but the iframe is still "broken" (e.g. has the wrong size).
- The JS does some styling magic to extend the iframe to the available width.
- The JS needs to run when the iframe is in an unblocked state — otherwise its size calculation fails.

**How to fix**

Block the JS too — even though it doesn't set any cookies — and attach it to the same service as the iframe. The JS will then run *after* the iframe is unblocked.

Migration guide
---------------

[](#migration-guide)

For upgrade notes between major versions, see [`MIGRATIONS.md`](./MIGRATIONS.md).

Contributing
------------

[](#contributing)

For test, build, and translation workflows, see [`CONTRIBUTING.md`](./CONTRIBUTING.md).

###  Health Score

56

—

FairBetter than 97% of packages

Maintenance90

Actively maintained with recent releases

Popularity36

Limited adoption so far

Community20

Small or concentrated contributor base

Maturity66

Established project with proven stability

 Bus Factor2

2 contributors hold 50%+ of commits

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 ~95 days

Recently: every ~133 days

Total

22

Last Release

35d ago

Major Versions

1.2.0 → 2.0.02021-07-12

2.0.0 → 3.0.02021-08-17

3.1.0 → 4.0.02021-12-16

4.4.3 → 5.0.02024-12-11

4.4.4 → 5.1.02026-01-21

### Community

Maintainers

![](https://www.gravatar.com/avatar/2ced0d63cfdae881c32128c7f66451a013d3e24d9eed210d6a846b6d8e95fa3b?d=identicon)[sandstorm](/maintainers/sandstorm)

---

Top Contributors

[![fheinze](https://avatars.githubusercontent.com/u/683773?v=4)](https://github.com/fheinze "fheinze (14 commits)")[![skurfuerst](https://avatars.githubusercontent.com/u/190777?v=4)](https://github.com/skurfuerst "skurfuerst (6 commits)")[![JamesAlias](https://avatars.githubusercontent.com/u/1615332?v=4)](https://github.com/JamesAlias "JamesAlias (5 commits)")[![erickloss](https://avatars.githubusercontent.com/u/16836464?v=4)](https://github.com/erickloss "erickloss (2 commits)")[![Pingu501](https://avatars.githubusercontent.com/u/12086990?v=4)](https://github.com/Pingu501 "Pingu501 (2 commits)")[![manumaticx](https://avatars.githubusercontent.com/u/2563642?v=4)](https://github.com/manumaticx "manumaticx (1 commits)")[![klfman](https://avatars.githubusercontent.com/u/9661367?v=4)](https://github.com/klfman "klfman (1 commits)")[![batabana](https://avatars.githubusercontent.com/u/36864084?v=4)](https://github.com/batabana "batabana (1 commits)")[![mberhorst](https://avatars.githubusercontent.com/u/2861236?v=4)](https://github.com/mberhorst "mberhorst (1 commits)")[![paavo](https://avatars.githubusercontent.com/u/1118783?v=4)](https://github.com/paavo "paavo (1 commits)")

---

Tags

cookie-consentneosneoscmscookieflowNeosprivacyconsentklaro.js

### Embed Badge

![Health badge](/badges/sandstorm-cookiepunch/health.svg)

```
[![Health](https://phpackages.com/badges/sandstorm-cookiepunch/health.svg)](https://phpackages.com/packages/sandstorm-cookiepunch)
```

###  Alternatives

[statikbe/laravel-cookie-consent

Cookie consent modal for EU

219426.2k](/packages/statikbe-laravel-cookie-consent)[techdivision/ckstyles

Neos package which enables you adding your custom style classes for the CkEditor with a simple Yaml configuration

21179.5k](/packages/techdivision-ckstyles)[shel/neos-colorpicker

A plugin for Neos CMS which provides a colorpicker editor

14104.7k6](/packages/shel-neos-colorpicker)[shel/neos-hyphens

A plugin for Neos CMS which provides hyphens for the inline editor

21214.3k1](/packages/shel-neos-hyphens)[moc/notfound

Neos CMS package that loads a normal editable page for displaying a 404 error

17170.4k](/packages/moc-notfound)[carbon/includeassets

Include your assets (css, js) in an easy way into Neos

14235.7k15](/packages/carbon-includeassets)

PHPackages © 2026

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