⚠️ Work in progress — createCMS is pre-1.0 and not production-ready (not tested in production). Expect breaking changes.
createCMS

Consent

Google Consent Mode v2 gating for analytics and embeds.

The consent plugin manages Google Consent Mode v2 signals on the client. It exposes the four consent signals, gates analytics on them, and provides a ConsentGate to gate embeds (a YouTube iframe, a map) behind a purpose. Until the visitor decides, analytics stay default-denied.

Installation

Add the server marker

The server plugin is a marker with no schema or endpoints; it registers consent as available:

lib/cms.ts
import { consent } from '@createcms/core/plugins/consent';

export const cms = createCMS({
  db,
  collections,
  media,
  plugins: [consent()],
});

Add the client plugin

All consent logic runs on the client:

lib/cms-client.ts
import { createCMSClient } from '@createcms/core/react';
import { consentClient } from '@createcms/core/plugins/consent/client';
import type { cms } from './cms';

export const cmsClient = createCMSClient<typeof cms>()({
  baseURL: '/api/cms',
  plugins: [consentClient()],
});

Usage

Set and read consent through the consent namespace. The four signals follow Consent Mode v2:

cmsClient.consent.setConsent({ analytics_storage: 'granted', ad_storage: 'denied' });

cmsClient.consent.isGranted('analytics_storage'); // boolean
ActionSignaturePurpose
setConsent(consent: Partial<ConsentState>) => voidUpdate one or more signals.
getConsent() => ConsentStateRead the current state.
isGranted(purpose) => booleanCheck one purpose.
isResolved() => booleanWhether consent has resolved (a real decision, or the default-deny timeout).
onChange(listener) => () => voidSubscribe to changes; returns an unsubscribe.
useConsentStateReact hookReturns { state, resolved, isGranted }.
reset() => voidRevoke in-session: reset all signals to denied. The gate stays resolved; nothing is persisted.

Gate an embed

ConsentGate renders its children only when the given purpose is granted, with an optional fallback:

const { ConsentGate } = cmsClient.consent;

<ConsentGate purpose="ad_storage" fallback={<p>Accept cookies to view this.</p>}>
  <iframe src="https://www.youtube.com/embed/..." />
</ConsentGate>;

Driving the gate from a CMP

The plugin is CMP-agnostic. It auto-reads Consent Mode v2 commands off window.dataLayer, so any CMP that emits them — Cookiebot, Usercentrics, OneTrust, or c15t via Google Tag Manager — drives the gate with no extra wiring. (You can also call setConsent from the CMP's update callback, which is the most reliable path under GTM.)

The c15t adapter

For c15t specifically — or any setup where you'd rather drive the gate explicitly than via dataLayer (e.g. c15t's offline mode) — an optional adapter at @createcms/core/plugins/consent/c15t maps c15t's consent categories to the four signals and pushes the decision into the gate:

components/consent-bridge.tsx
import { useConsentManager } from '@c15t/react';
import { useC15tConsentBridge } from '@createcms/core/plugins/consent/c15t';
import { cmsClient } from '@/lib/cms-client';

// Render inside c15t's <ConsentManagerProvider>.
export function ConsentBridge() {
  const { consents, hasConsented } = useConsentManager();
  useC15tConsentBridge(cmsClient, { consents, hasConsented });
  return null;
}

The default mapping is measurementanalytics_storage and marketingad_storage / ad_user_data / ad_personalization (necessary / functionality / experience are ignored); pass a third argument to override it. The adapter takes c15t's consent record as input and has no @c15t/* dependency, so it works with any c15t version. The pure mapper consentModeFromC15t(consents, mapping?) is exported too.

State

ConsentState has four signals, each 'granted' or 'denied':

SignalPurpose
analytics_storageAnalytics cookies and storage.
ad_storageAdvertising storage.
ad_user_dataSending user data for ads.
ad_personalizationAd personalization.

The consent plugin adds no database schema.

Why client-only

Consent originates in the browser, where the consent banner runs and where device storage is gated. The signals must be set before any analytics or storage access, so the gate lives on the client. Server-side tracking (the A/B plugin's GA4 forwarding) is gated independently.

On this page