fendinger / kirby-consent-gate
GDPR compliant external content blocker for Kirby CMS
Package info
github.com/fendinger/kirby-consent-gate
Type:kirby-plugin
pkg:composer/fendinger/kirby-consent-gate
Requires
- getkirby/cms: ^5.0
README
Kirby Consent Gate
This plugin for Kirby CMS automatically detects and blocks external content (iframes, scripts, embeds) from loading until the user gives consent. Fully GDPR compliant.
Unlike traditional consent plugins that require manual tag overrides, this plugin parses the rendered HTML output and automatically wraps any external resource in a consent gate - no matter how the content was added (blocks, tags, templates, raw HTML).
This plugin does not use cookies. Consent is stored in the browser's local storage.
This is the successor to kirby3-consent-gate, completely rewritten for modern Kirby versions.
How It Works
- HTML Parser scans the rendered page for external
<iframe>,<script>,<embed>and<object>elements - Visual embeds (iframes, embeds, objects) are replaced with a consent gate UI showing the vendor's logo, name and privacy information
- Scripts (e.g. Google Analytics) are neutralized with
type="text/plain"and activated on consent - Compound embeds (e.g. Twitter blockquote + script) are detected as a unit and gated together
- A vendor registry tracks which vendors were detected across all pages and displays them in the consent banner/modal
Features
- Automatic detection of external content - no manual tag overrides needed
- Supports iframes, scripts, embeds and objects
- Compound embed detection (Twitter/X, Instagram)
- Per-vendor consent via banner and modal
- Vendor registry across all pages
- Cookie-free (uses localStorage)
- Standalone CSS or Tailwind CSS support
- Built-in vendor configs with logos for 15+ services
- Vendor allowlist to skip gating for specific vendors
- English and German translations
- Custom vendor configs via
site/config/config.php
Supported Vendors (built-in)
| Vendor Key | Name | URL Patterns |
|---|---|---|
bluesky |
Bluesky | bsky.app, bsky.social |
facebook |
facebook.com, fbcdn.net |
|
flickr |
Flickr | flickr.com, staticflickr.com |
github |
GitHub | github.com, githubusercontent.com |
googleanalytics |
Google Analytics | googletagmanager.com, google-analytics.com |
googlemaps |
Google Maps | google.*/maps/ |
instagram |
instagram.com, cdninstagram.com |
|
kaltura |
Kaltura | kaltura.com, kaltura.org |
mastodon |
Mastodon | mastodon.social, mastodon.online |
matomo |
Matomo | (self-hosted, add your domain via config) |
openstreetmap |
OpenStreetMap | openstreetmap.org, osm.org |
spotify |
Spotify | spotify.com |
vimeo |
Vimeo | vimeo.com |
x |
X/Twitter | x.com, twitter.com, twimg.com |
youtube |
YouTube | youtube.com, youtu.be, youtube-nocookie.com, ytimg.com |
Unknown external sources are automatically gated as unknown:hostname (configurable via gateUnknown).
Installation
Composer
composer require fendinger/kirby-consent-gate
Manual
Download and copy this repository to /site/plugins/kirby-consent-gate.
Configuration
All options are set in site/config/config.php:
Style
Choose between standalone CSS (default) or Tailwind CSS:
// Standalone CSS (default) - no dependencies 'fendinger.consent-gate.style' => 'css', // Tailwind CSS - assumes Tailwind is active in your project 'fendinger.consent-gate.style' => 'tailwind',
Allowlist
Vendors that should never be gated (by vendor key):
'fendinger.consent-gate.allowlist' => [ 'youtube', 'x', ],
For unknown vendors, use the unknown:hostname format:
'fendinger.consent-gate.allowlist' => [ 'unknown:cdn.example.com', ],
Gate Unknown Sources
Whether to gate external sources that don't match any known vendor:
'fendinger.consent-gate.gateUnknown' => true, // default
Gate Elements
Which HTML element types to scan:
'fendinger.consent-gate.gateElements' => ['iframe', 'script', 'embed', 'object'], // default
Privacy Page
Slug of the page to link as privacy policy in the consent modal:
'fendinger.consent-gate.privacyPage' => 'datenschutz', // default
Custom Vendors
Add or override vendor configurations:
'fendinger.consent-gate.vendors.tiktok' => [ 'name' => 'TikTok', 'urlPatterns' => ['tiktok\.com'], 'compoundPattern' => '<blockquote[^>]*class="[^"]*tiktok-embed[^"]*"[^>]*>.*?</blockquote>\s*<script[^>]*src="[^"]*tiktok\.com/embed\.js[^"]*"[^>]*>\s*</script>', 'logo' => '<svg>...</svg>', 'description' => [ 'en' => '<p>TikTok videos are embedded on our website...</p>', 'de' => '<p>Auf unserer Website werden TikTok-Videos eingebunden...</p>', ], 'privacy' => 'https://www.tiktok.com/legal/privacy-policy', ],
Vendor Config Options
| Option | Description |
|---|---|
name |
Display name |
urlPatterns |
Array of regex patterns matched against element URLs |
compoundPattern |
Optional regex for multi-element embeds (e.g. blockquote + script) |
logo |
SVG markup (48x48) |
description |
Array with en and de keys containing HTML descriptions |
privacy |
URL to the vendor's privacy policy |
How the Parser Works
The plugin hooks into Kirby's page.render:after event and processes the HTML in two steps:
- Compound embeds are detected first via regex patterns (e.g. Twitter's
<blockquote>+<script>combo) and wrapped in a gate - Individual elements are parsed with PHP's
DOMDocument. For each<iframe>,<script>,<embed>or<object>with an external URL:- The URL is checked against the site's own domain
- The matched vendor is checked against the allowlist
- The URL is matched against vendor
urlPatterns - Iframes/embeds/objects are replaced with the gate UI
- Scripts are neutralized with
type="text/plain"and reactivated via JS on consent
Vendor Registry Updates
The vendor registry stays in sync automatically through the following hooks:
page.render:after— on every frontend visit, detected vendors for that page are written to the registrypage.create:after— newly created pages are rendered immediately so their vendors get registered right awaypage.update:after— updated pages are re-rendered so their registry entry reflects the latest contentpage.changeSlug:after— the old page ID is removed from the registry and the page is re-rendered under its new IDpage.delete:after— the deleted page's entry is removed from the registry
Because creating, updating or renaming a page triggers a full render inside the Panel request, saving may take slightly longer on very heavy pages.
Manual crawl
If the registry is ever out of sync (e.g. after a bulk content import or after installing the plugin on an existing site), you can trigger a full crawl via:
GET /consent-gate/crawl
This iterates over all pages, renders each one and returns a JSON summary with the number of rendered pages, any failures and the aggregated list of detected vendors.
Limitations
The parser runs on Kirby's page.render:after hook, which only fires when a Kirby\Cms\Page is actually rendered. As a result, the following cases are not covered:
- Custom routes defined in
site/config/config.phpthat return HTML directly (without calling$page->render()orsite()->visit()) - Plugin routes that return their own
Response - API / JSON endpoints
- Any other output that bypasses Kirby's page rendering pipeline
External resources embedded in such responses will not be gated automatically. If you use custom routes to deliver HTML, you will need to handle consent for external content there manually.
Support
If you find this plugin useful, you can support the development:
License
MIT