Skip to main content

Documentation

No results found.
Features

Cookie Consent

WebProCMS ships with a built-in cookie consent system that handles GDPR (EU/UK/Switzerland), CCPA/CPRA (California), and similar privacy laws around the world. It's off by default and adds zero overhead to sites that don't use it. When enab...

WebProCMS ships with a built-in cookie consent system that handles GDPR (EU/UK/Switzerland), CCPA/CPRA (California), and similar privacy laws around the world. It's off by default and adds zero overhead to sites that don't use it. When enabled, it shows a configurable consent banner, gates third-party tracking scripts until consent is granted, replaces third-party embeds (YouTube, Vimeo, Loom, Google Maps, styled maps) with click-to-load placeholders, and keeps a Cookie settings button on every page so visitors can change or withdraw their choice.


When to turn it on

The legal triggers are based on the visitor's location, not where the business is located:

  • GDPR / UK GDPR / Swiss FADP — Required for any visitor from the EEA, UK, or Switzerland if the site uses non-essential cookies (Google Analytics, Meta Pixel, ad networks, YouTube embeds, etc.). There is no revenue threshold — even a single EU visitor counts.
  • CCPA / CPRA — Required for businesses serving California residents that cross at least one threshold: $25M+ annual revenue, or buying/selling/sharing personal info of 100,000+ CA consumers per year, or deriving 50%+ of revenue from selling/sharing CA consumer data. Most small businesses fall under the thresholds — but using Google Analytics + ad pixels often pushes them over the "sharing" definition.
  • Most US states / rest of world — No federal law in the US yet; many other countries have GDPR-style rules.

Rule of thumb: if the site uses Google Analytics, Meta Pixel, ad pixels, YouTube embeds, or any third-party tracking — turn it on. The cost of an unnecessary banner is near zero; the cost of being audited without one is a percentage of revenue.

How it works

Six pieces, all built into the CMS:

  1. Consent banner — A built-in Alpine component themed by branding tokens. Renders on every public page when the feature is enabled. Offers Accept All / Reject All / Manage Preferences with per-category toggles (Necessary / Analytics / Marketing).
  2. Cookie settings button — A small round button in the bottom-left control cluster (beside the dark-mode toggle and language switcher, when those are on) that reopens the banner. See Changing or withdrawing consent.
  3. Script gating — Every snippet on the Dashboard → Snippets page has a Consent Category dropdown. Snippets in the Analytics or Marketing categories render as inert <template> blocks in the HTML; a small JS shim moves them into the live DOM (and executes any <script> tags) once the visitor grants consent for that category.
  4. Embed gating — <x-dl.video> and rich-text videos (YouTube, Vimeo, Loom) render a placeholder with an "Accept & load" button when the marketing category isn't consented. The iframe URL lives in data-src so the iframe never loads — and never sets cookies — until the visitor explicitly opts in. A YouTube placeholder shows the video's poster, served from your own site so the visitor's browser doesn't contact YouTube before consent. YouTube embeds also use youtube-nocookie.com even when consented, which is YouTube's privacy-enhanced domain. Maps are gated the same way — see Third-party embeds.
  5. Built-in GA4 relay (Settings → Analytics → Integrations) — when a GA4 Measurement ID is configured, the gtag.js bootstrap auto-injects into <head> and is wrapped in the same consent-template shim, so GA's cookies don't load until the visitor grants the Analytics category. See addons/analytics.md.
  6. Server-side consent check (CookieConsent::hasGranted) — non-snippet integrations (currently: the analytics persistent visitor cookie) read wpcms_consent server-side to decide whether to act. The helper reads the cookie, falls back to the mode's default-granted set when no cookie is present (everything for opt-out; only Necessary for opt-in and off), and returns true when the feature is OFF so disabled installs behave as if everything is granted.

Modes

Mode Behavior When to use
Auto-detect by visitor country (recommended) EEA/UK/CH visitors get opt-in (GDPR). US visitors get opt-out (CCPA/CPRA). Other countries get the configured Default for other countries. Visitors whose country can't be determined get opt-in. Default for any production site. Works with or without a CDN — see Country detection.
Opt-in for everyone Scripts blocked until consent. Strictest mode. Sites with significant EU traffic, or operators who'd rather over-comply.
Opt-out for everyone Scripts run by default; banner explains how to reject. US-only sites with no expected EU traffic.
Off System loaded, snippet/embed gating still respected, but no banner is shown and nothing fires automatically. Gated content stays blocked until the visitor accepts from an embed's placeholder or the Cookie settings button. Staging environments, or testing that the gating works before going live.

For Auto mode, a visitor from a known country outside the EU/UK/CH and the US gets the configurable Default for other countries (off, opt_out, or opt_in). A visitor whose country can't be determined always gets opt-in — the banner fails closed, never open.

Do Not Track and Global Privacy Control

A browser that sends Do Not Track or Global Privacy Control (GPC — the opt-out signal California's CCPA and several other US state laws require sites to honour as a "do not sell or share") has already said no. So in opt-out mode it starts with only the necessary category granted, the same as opt-in: no analytics or marketing snippet, map or embed fires until the visitor accepts. The banner still shows, with its toggles off, and an explicit choice there wins. The server decides it in CookieConsent::defaultGrantedCategories() (and the init response Varys on DNT / Sec-GPC); the shim applies the same rule in the browser, which covers a browser-cached init answer and the fail-closed fallback. Opt-in and off already grant only necessary. Separately from consent, the built-in analytics and the GA4 tag never track a signalling browser at all.

Country detection

Auto mode finds the visitor's country in this order:

  1. CDN header — Cloudflare's CF-IPCountry, CloudFront's CloudFront-Viewer-Country, or Vercel's X-Vercel-IP-Country. Free and instant.
  2. Local IP-to-country database — when there's no CDN header, the visitor's IP is looked up in a database file stored on the server (DB-IP Lite by default, the same one the form spam filter uses). Nothing is sent to a third party. The database downloads automatically when you save Cookie Consent in Auto mode on a site with no CDN header, and the consent init endpoint retries the download (at most once an hour) if it's missing. A weekly job keeps it up to date. You can also install it by hand with php artisan geo:install.
  3. Unknown — no header and no database entry (including private/local addresses): the visitor gets opt-in.

Architecture: how the cache stays intact

WebProCMS uses Spatie's response cache for the public site — every page is cached once and served to every visitor. The consent system is designed to never multiply or invalidate that cache by visitor.

The cached HTML always contains the inert form of every gated thing:

  • Gated snippets render as <template data-consent-category="X" data-consent-placement="Y">…</template> — the browser ignores these completely.
  • Gated YouTube/Vimeo embeds render as <iframe data-src="…"> plus a visible placeholder — the iframe never loads.
  • The banner shell is in every cached page as display:none, and the Cookie settings button is in every cached page too.

All per-visitor variance happens client-side:

  • Cookie check — the JS shim reads wpcms_consent cookie on page load. Zero server round trip.
  • First-visit mode resolution — on the first session visit (no cookie yet), the shim hits the uncached /_cookie-consent/init endpoint, which returns {mode, country, …} based on the visitor's country (Country detection). The browser caches this response for 30 minutes (Cache-Control: private, max-age=1800), so subsequent page loads are zero extra requests. If the request fails, the shim falls back to opt-in: the banner shows and everything stays gated.

Net result: one cached HTML serves every visitor. Activation, banner visibility, and mode detection all happen in ~1KB of JS in the visitor's browser.

Snippet categories

Every entry on the Snippets page has a Consent Category:

  • Necessary — Always runs, never gated. Use for: session/CSRF cookies, site-verification meta tags, structured-data JSON-LD, dark-mode preference persistence.
  • Analytics — Runs only after the visitor grants analytics consent. Use for: Google Analytics, Plausible, Mixpanel, session replay tools.
  • Marketing — Runs only after the visitor grants marketing consent. Use for: Google Tag Manager (when used for ads), Meta Pixel, LinkedIn Insight Tag, Google Ads conversion tracking, TikTok Pixel.

When the cookie consent feature is off, the category is ignored — every active snippet runs, exactly like today. Existing snippets default to Necessary on migration, so enabling the feature without re-categorizing won't break anything — but it also won't gate anything until you set the right categories.

Consent plus delayed loading. A snippet can also be set to Loading: Delayed (Delayed snippets). A gated, delayed snippet renders as ONE template carrying both data-consent-category and data-snippet-delay. The shim waits for consent, then hands the snippet to the delay: the visitor's first interaction, or N seconds after load. Clicking Accept (or an embed's Accept & load) counts as that interaction, so a snippet the visitor has just allowed runs at once. The shim and the delay share one activator (snippet-templates.js). It re-creates scripts with their exact bodies (so the security policy's hashes still match), loads a library before the code that calls it, and never runs a snippet twice.

Third-party embeds

When the feature is enabled, every embed that would contact a third party waits for marketing consent. Nothing in a placeholder loads from the third party either.

Videos — <x-dl.video> (embed mode) and videos in rich-text bodies (<x-inline-video>: blog posts, course lessons):

  • If the visitor hasn't granted marketing consent yet, the iframe URL is stored in data-src and a placeholder is rendered instead. No iframe = no third-party cookies dropped.
  • A YouTube placeholder shows the video's poster behind the prompt. The poster is served from your site at /_embed-thumb/youtube/{signature}/{id}.jpg: the server fetches it from YouTube once, keeps it in storage/app/private/embed-thumbnails/, and serves that copy to every visitor. The path is signed per video, so the route only fetches posters for videos your pages actually show. Vimeo and Loom placeholders have no poster.
  • The placeholder shows a configurable message + two buttons: Accept & load (grants marketing consent and activates this embed plus any other gated content site-wide) and Open on original site instead (opens the video URL in a new tab — never loads the embed).
  • When consent is granted, the JS shim swaps data-src → src on the iframe and hides the placeholder. The iframe loads using youtube-nocookie.com (YouTube) or the standard Vimeo/Loom player.

Maps — <x-dl.map> and the real-estate search map:

Map Third party With consent on
Google embed (iframe) Google (sets cookies) iframe in data-src + placeholder
Google (keyed JavaScript API) Google (sets cookies) container marked data-consent-pending + placeholder
Styled map (Leaflet) Esri map tiles (see the visitor's IP and the area viewed) container marked data-consent-pending + placeholder
Real-estate listing map Esri map tiles container marked data-consent-pending + placeholder
OpenStreetMap embed (iframe) OpenStreetMap (no cookies, but sees the visitor's IP) iframe in data-src + placeholder

A data-consent-pending map is skipped by its JavaScript until the visitor grants marketing consent; the shim then removes the marker and placeholder and fires dl:consent-maps-unlocked, and the map draws. The listing map keeps any search results that arrived while it was waiting.

Adding a new embed: an iframe uses the data-consent-category="marketing" wrapper with data-src on the iframe and a placeholder div (video.blade.php is the reference). A script-rendered embed uses data-consent-pending="marketing" plus a placeholder and skips pending containers in its boot (styled-map.js). Either way, render the gate only when CookieConsent::enabled(), and never load an image or script from the third party inside the placeholder.

While the feature is on, every public page shows a Cookie settings button in the bottom-left corner. It's part of the layout, not the footer, so it's there even on pages with no footer or a custom footer. Clicking it (or any element with a data-consent-open attribute, such as a footer link you add) reopens the banner with the visitor's current choices.

When a visitor takes away a category that's already active — Reject all after accepting, or switching a category off in Manage preferences — the new choice is saved and the page reloads, because scripts that already ran can't be stopped any other way. The reloaded page only activates what the visitor now allows.

The button's label is set under Settings → Privacy & Compliance → Cookie Consent → Other and translates per site language like the rest of the banner copy.

Configuration

All settings live under Dashboard → Settings → Privacy & Compliance → Cookie Consent:

  • Enable/disable toggle
  • Mode (Auto / Opt-in / Opt-out / Off)
  • Default for other countries (Auto mode only)
  • Banner position (Bottom bar / Bottom-left card / Bottom-right card / Center modal)
  • Banner copy (title, body, accept/reject/manage/save labels)
  • Embed placeholder copy (message, accept-and-load label, external-link label)
  • Privacy policy URL (linked from the banner; blank follows the site's privacy policy page wherever it lives — see Smart Privacy Policy)
  • Cookie settings button label
  • Cookie lifetime in days (default 180)

Saving these settings clears the public response cache so the next visitor sees the new banner/markup immediately.

What it's not

  • Not a privacy policy generator. Admins are responsible for their own privacy policy page (a starter template ships in the design library under privacy-policy).
  • Not legal advice. The categories, copy, and mode mappings reflect common interpretations of the law — operators should consult counsel if their jurisdiction or use case is unusual.
  • Not a GeoIP service. Country detection uses a CDN header when there is one and otherwise a free local database (see Country detection); no paid lookup service is called. Visitors the database can't place get opt-in.
  • Not a consent management platform. Categories are fixed at three (Necessary / Analytics / Marketing). There's no per-vendor disclosure UI like Cookiebot's "this site uses N vendors" view. The system is built for the common case of a small-to-medium business site, not for ad-network publishers running 200+ vendor scripts.