Calucon Third-Party Embed Gate — API reference

Every filter and action Calucon Third-Party Embed Gate fires, with its signature, firing point, return contract, parameters and a runnable example. It also covers the settings schema, the WP-CLI commands, the placeholder markup contract, the data attributes, the CSS custom properties and the front-end JavaScript surface.

Version 1.0.0: 15 filters and 3 actions, documented from the plugin source. Requires WordPress 5.9 and PHP 7.4. No hook is required for the plugin to operate. The same reference ships in the plugin as docs/customizing.md.


Where the code goes

Sixteen of the eighteen hooks may be registered anywhere that runs before content is rendered: a child theme’s functions.php, a site-specific plugin, or WP-CLI. Plugin files must not be edited; updates overwrite them.

The remaining two, calucon_embed_gate_the_content_priority and calucon_embed_gate_render_block_priority, fire during plugin registration on plugins_loaded, before themes load. A callback registered in functions.php is never invoked and no error is raised. Both require an mu-plugin:

<?php
/**
 * Plugin Name: Embed gate tweaks
 * Description: Site-specific adjustments to the embed gate.
 */

// wp-content/mu-plugins/embed-gate-tweaks.php
// Files directly inside mu-plugins/ load before
// plugins_loaded, which is early enough for
// every hook on this page.

add_filter(
	'calucon_embed_gate_the_content_priority',
	function ( $priority ) {
		return 60;
	}
);

calucon_embed_gate_providers is unaffected: the provider registry resolves lazily, on first use, so theme-registered descriptors reach it.


The two shared arguments

Received by most hooks. calucon_embed_gate_payload receives $provider without $ctx.

$provider — the descriptor

A normalised provider descriptor. Every key is present regardless of what the source descriptor set; defaults are applied before any consumer sees it.

Descriptor keys (19)
id string
Stable identifier, generic for unrecognised hosts. The field to branch on — it is never translated, never empty and not overridable by the site owner.
label string
Human-readable name, shown in the panel and the settings table. Never translated — provider names are proper nouns, so this is stable across languages.
enabled bool
Whether the site owner has this provider switched on.
kind string
One of video, map, audio, social, form, calendar, document, image, 3d, or empty. Chooses the optional button glyph.
match array
How URLs are recognised: iframe_host, iframe_path, iframe_query, script_host, script_path.
strategy string
iframe or script — which shape of embed this is.
load_host, load_path, load_query
The privacy-preserving target used after the click, when the provider offers one. This is how Vimeo gets dnt=1. Null or empty means “load the URL the embed carried”.
fallback string
Template for the no-JavaScript link, with {id}-style tokens filled from the match’s named captures.
privacy_url string|null
The provider’s own privacy policy. Shown in the panel only when the site owner enables that link.
controller string|null
Who operates the service, as a plain name and place.
note, action string
Default panel text and button text before the wording hooks run. These two are translated, and the site owner can override both from the settings screen — so their contents are not something to compare against.
aspect string|null
Declared aspect ratio such as 16/9, used to reserve space so the page does not jump when the embed loads.
iframe_allow string|null
Permissions policy applied to the rebuilt iframe. autoplay is stripped from it regardless.
companion_class, companion_fallback, scrub_hint_hosts
For script embeds: the sibling markup that belongs to the same embed, how to build its fallback link, and which CDN hosts must have their resource hints removed.

Branch on id. note and action are translated, and both can be overridden by the site owner, so neither is reliable for comparison. label and controller are not translated. id is never empty.

Authoritative list and defaults: src/Providers/Provider.php, in normalize().

$ctx — where the embed was found

The integration context. Keys differ by path: four exist only for block-rendered embeds, and the admin preview carries only integration. Test with isset() or ! empty() before reading any key except integration.

Context keys (7)
integration string, always present
Which path found the embed: render_block, the_content, comment, widget, description, output_buffer or admin-preview.
post_id int|null
The post being rendered, on the render_block, the_content and comment paths. Null for widgets, descriptions and whole-page buffering, which have no single post. Absent entirely on admin-preview.
block string|null
The block name, on render_block only. Null on the other content paths, absent on admin-preview.
force_gate bool, render_block only
The editor’s Always gate override. When true the detection rules skip calucon_embed_gate_should_gate altogether.
poster string, render_block only
A poster image URL chosen in the editor, present only when one is set and only after the server has confirmed it resolves to this site.
note_text, action_text string, render_block only
Per-block overrides from the editor, present only when set. Capped at 400 and 120 characters respectively — note that the equivalent per-provider settings are capped at 500.

What runs when

Order of execution for a single embed, from detection to markup.

  1. Detection selects a candidateAn iframe, script, object or image on a host that is not the site’s.
  2. Gate, or allow throughis_own_host · own_hosts · www_equivalence · should_gateLast point at which an embed can be allowed through. Blocks marked Always gate bypass should_gate.
  3. Provider resolutionproviders (once per request) · provider_for_urlDetermines name, load target and fallback link. Does not affect gating.
  4. Panel construction beginsbefore_renderNo markup exists yet.
  5. Text resolutionnote_text · action_text · fallback_urlPlain text in, plain text out. Escaping occurs after these.
  6. Payload and markuppayload · placeholder_htmlThe payload is what the browser rebuilds from. The HTML is printed unescaped.
  7. Embed gatedembed_gatedFired once the panel exists. The front-end assets are enqueued at the same point.
  8. Activation, in the browserNo PHP hook runs on activation, and no JavaScript event is dispatched. See front-end JavaScript.

Describing a provider

Determine how an embed is described: its name, its privacy-preserving load target and its fallback link. Neither affects whether an embed is gated.

calucon_embed_gate_providers

Register or replace provider descriptors.

apply_filters( 'calucon_embed_gate_providers', array $providers )

filter1 argument

Fires
Once per request, when the provider registry is first resolved. Not at plugins_loaded.
Returns
The full descriptor array. Append to the array received and return it.

Parameters

$providers array
The built-in descriptors, already normalised and already passed through the plugin’s translator.

Owner-defined rows and the per-provider settings are applied after this filter. A site owner’s notice text, button text, privacy URL and enabled flag therefore override a code-registered descriptor.

Compare on id. The note and action values are translated before this filter runs; label and controller are not.

Example

add_filter(
	'calucon_embed_gate_providers',
	function ( array $providers ) {
		$providers[] = array(
			'id'          => 'example-videos',
			'label'       => 'ExampleVideos',
			'kind'        => 'video',
			'match'       => array(
				'iframe_host' => array( 'embed.example.com' ),
				// Named captures interpolate,
				// URL-encoded, into load_path
				// and fallback as {id}-style
				// tokens.
				'iframe_path' => '#^/v/(?<id>[A-Za-z0-9_-]+)#',
			),
			// Optional privacy-preserving
			// target, used after the click.
			'load_host'   => 'embed-nocookie.example.com',
			'load_path'   => '/v/{id}',
			'fallback'    => 'https://example.com/watch/{id}',
			'controller'  => 'Example Ltd, Berlin, Germany',
			'privacy_url' => 'https://example.com/privacy',
			'aspect'      => '16/9',
		);
		return $providers;
	}
);

Fired at src/Plugin.php:279. Permalink

calucon_embed_gate_provider_for_url

Override the descriptor matched to a URL.

apply_filters( 'calucon_embed_gate_provider_for_url', array $provider, string $url, string $host )

filter3 arguments

Fires
Once per detected embed, after the registry matches a descriptor and before rendering.
Returns
The descriptor to use. Cast to array.

Parameters

$provider array
The descriptor the registry matched. For an unrecognised host this is the generic one.
$url string
The embed URL as found in the content — except from resolve_for_asset_host(), which passes a synthetic https://host/. See the note.
$host string
That URL’s host, lower-cased.

Affects only how an embed is described. Gating is unaffected: an unrecognised host is gated either way.

One caller hands over a URL that never appeared in the markup. Matching a provider by asset host alone — a stylesheet, or since 1.0.0 the check that stops a known provider from hiding behind a /wp-content/ path — has only a host to work with, so it synthesises https://host/ for this filter. Code here that reads $url for a path, a query string or an id must tolerate that: match on $host first, and treat a bare origin as no information rather than as a URL that failed to match.

Example

add_filter(
	'calucon_embed_gate_provider_for_url',
	function ( array $provider, $url, $host ) {
		// One tenant's videos share a host with
		// everyone else's, so the label has to
		// come from the path.
		if ( 'generic' === $provider['id']
			&& false !== strpos( $url, '/studio-a/' ) ) {
			$provider['label'] = 'Studio A';
		}
		return $provider;
	},
	10,
	3
);

Fired at src/Plugin.php:427. Permalink


Wording and links

Plain text in, plain text out. Escaping occurs after these hooks; a pre-escaped return value reaches the visitor as entities.

calucon_embed_gate_note_text

Replace the notice text on a panel.

apply_filters( 'calucon_embed_gate_note_text', string $note, array $provider, array $ctx )

filter3 arguments

Fires
Once per gated embed, while its panel is built.
Returns
Plain text. The panel escapes the return value; do not pre-escape.

Parameters

$note string
The notice text as resolved so far: the provider default, or the site owner’s override if one is set.
$provider array
The descriptor. See the shared arguments.
$ctx array
Where this embed was found. See the shared arguments.

The plugin is a technical measure. Notice text should not assert a legal outcome.

Example

add_filter(
	'calucon_embed_gate_note_text',
	function ( $note, array $provider, array $ctx ) {
		// Plain text in, plain text out. The
		// panel escapes this, so an &amp; here
		// would reach the visitor as
		// "&amp;amp;".
		if ( 'youtube' === $provider['id'] ) {
			return 'Video hosted by YouTube. It loads when you '
				. 'choose to load it.';
		}
		return $note;
	},
	10,
	3
);

Fired at src/Plugin.php:449. Permalink

calucon_embed_gate_action_text

Replace the button label on a panel.

apply_filters( 'calucon_embed_gate_action_text', string $action, array $provider, array $ctx )

filter3 arguments

Fires
Once per gated embed, while its panel is built.
Returns
Plain text. The panel escapes the return value; do not pre-escape.

Parameters

$action string
The button text resolved so far.
$provider array
The descriptor.
$ctx array
The integration context.

The label is the accessible name of a <button> and should describe the action, for example “Load this video”.

Example

add_filter(
	'calucon_embed_gate_action_text',
	function ( $action, array $provider, array $ctx ) {
		// $ctx['integration'] is the one key
		// that is always present, whichever
		// path the embed came in on.
		if ( 'comment' === $ctx['integration'] ) {
			return 'Load this embed';
		}
		return $action;
	},
	10,
	3
);

Fired at src/Plugin.php:452. Permalink

calucon_embed_gate_fallback_url

Replace the target of the no-JavaScript link.

apply_filters( 'calucon_embed_gate_fallback_url', string $url, array $provider, array $ctx )

filter3 arguments

Fires
Once per gated embed, while its panel is built.
Returns
A URL, used for the server-rendered link and for the error state. An empty string leaves no link.

Parameters

$url string
The fallback the descriptor produced, usually built from the embed’s own id.
$provider array
The descriptor.
$ctx array
The integration context.

This link is the only route to the content when JavaScript is unavailable.

Example

add_filter(
	'calucon_embed_gate_fallback_url',
	function ( $url, array $provider, array $ctx ) {
		// Prefer your own write-up over the
		// provider's page, when the post has
		// one. post_id is absent on the widget,
		// description and output-buffer paths.
		if ( ! empty( $ctx['post_id'] ) ) {
			$own = get_post_meta(
				$ctx['post_id'],
				'embed_writeup_url',
				true
			);
			if ( is_string( $own ) && '' !== $own ) {
				return $own;
			}
		}
		return $url;
	},
	10,
	3
);

Fired at src/Plugin.php:455. Permalink


Markup and payload

The inverse of the wording hooks: return values are printed unescaped. Escape any interpolated value.

calucon_embed_gate_placeholder_html

Rewrite the rendered panel markup.

apply_filters( 'calucon_embed_gate_placeholder_html', string $html, array $provider, array $ctx )

filter3 arguments

Fires
Once per gated embed, after the placeholder template has rendered.
Returns
HTML, printed unescaped. Escape any interpolated value.

Parameters

$html string
The rendered panel.
$provider array
The descriptor.
$ctx array
The integration context.

Four parts are contractual and are located by class: the cg-embed container with its data-cg-* attributes, the cg-embed__panel div directly inside it, a <button type="button" class="cg-embed__button">, and the cg-embed__fallback paragraph.

Must not emit a literal <iframe: gated content may be processed again, and a raw iframe is re-detected. To replace the markup wholesale, override templates/placeholder.php in the theme.

Example

add_filter(
	'calucon_embed_gate_placeholder_html',
	function ( $html, array $provider, array $ctx ) {
		if ( 'video' !== $provider['kind'] ) {
			return $html;
		}
		// Wrapping keeps every class and
		// attribute the front end relies on
		// intact. Whatever is returned is
		// printed, so the caption is escaped
		// here.
		$caption = sprintf(
			/* translators: %s: provider name. */
			__( 'Hosted by %s.', 'my-theme' ),
			$provider['label']
		);
		return '<figure class="video-embed">'
			. $html
			. '<figcaption>' . esc_html( $caption )
			. '</figcaption></figure>';
	},
	10,
	3
);

Fired at src/Plugin.php:434. Permalink

calucon_embed_gate_payload

Adjust the JSON the browser rebuilds from.

apply_filters( 'calucon_embed_gate_payload', array $payload, array $provider )

filter2 arguments

Fires
Once per gated embed, after the payload is assembled and before it is encoded.
Returns
The payload array. Cast to array, then JSON-encoded into an attribute; the value must be serialisable.

Parameters

$payload array
What the browser rebuilds the embed from: src, attrs, and depending on the embed srcdoc, tag, strategy or inline. See the payload.
$provider array
The descriptor.

No $ctx is passed. Use placeholder_html where the integration context is required.

attrs is an array when attributes are present and an empty stdClass otherwise, so that it encodes as {} rather than []. Check with is_array() before writing to it.

Example

add_filter(
	'calucon_embed_gate_payload',
	function ( array $payload, array $provider ) {
		// attrs is an empty stdClass when the
		// embed carried no attributes, so it
		// cannot be indexed blindly.
		if ( ! is_array( $payload['attrs'] ) ) {
			$payload['attrs'] = array();
		}
		if ( ! isset( $payload['attrs']['title'] ) ) {
			$payload['attrs']['title'] = sprintf(
				/* translators: %s: provider name. */
				__( 'Embedded content from %s', 'my-theme' ),
				$provider['label']
			);
		}
		return $payload;
	},
	10,
	2
);

Fired at src/Plugin.php:437. Permalink


Plumbing

Ordering and asset versioning. The two priority filters are the only hooks on this page that cannot be used from a theme.

calucon_embed_gate_cmp_config

Adjust or disable the consent-platform bridge.

apply_filters( 'calucon_embed_gate_cmp_config', ?array $config, array $cmp_options )

filter2 arguments

Fires
On a page with gated embeds, when the bridge configuration is assembled.
Returns
The configuration array, or null to disable the bridge. Any non-array value disables it.

Parameters

$config array|null
The built config, or null when the bridge is disabled or no tested platform was detected.
$cmp_options array
The cmp section of the settings: bridge, borlabs_group.

Disabling the bridge is restrictive rather than permissive: the server always renders the panel, so gated embeds then require a click.

Example

add_filter(
	'calucon_embed_gate_cmp_config',
	function ( $config, array $cmp_options ) {
		// null means the bridge is already off.
		// Leave it off.
		if ( ! is_array( $config ) ) {
			return $config;
		}
		// This site's platform files embeds
		// under its own category name rather
		// than the default.
		$config['category'] = 'external-media';
		return $config;
	},
	10,
	2
);

Fired at src/Plugin.php:763. Permalink

calucon_embed_gate_the_content_priority

Set the gate’s priority on the_content.

apply_filters( 'calucon_embed_gate_the_content_priority', int $priority )

filter1 argument

Fires
During plugin registration, on plugins_loaded.
Returns
The priority. Default 20. Cast to int.

Parameters

$priority int
The default, 20.

Not usable from a theme. Registration occurs on plugins_loaded, which precedes theme loading, so a callback added in functions.php never runs and no error is raised. Use an mu-plugin; see where the code goes.

Example

// wp-content/mu-plugins/embed-gate-priority.php
// An mu-plugin, not functions.php: this filter
// fires while the plugin registers on
// plugins_loaded, before any theme loads.

add_filter(
	'calucon_embed_gate_the_content_priority',
	function ( $priority ) {
		// Run after a plugin that rewrites
		// content at 50.
		return 60;
	}
);

Fired at src/Integration/TheContent.php:37. Permalink

calucon_embed_gate_render_block_priority

Set the gate’s priority on render_block.

apply_filters( 'calucon_embed_gate_render_block_priority', int $priority )

filter1 argument

Fires
During plugin registration, on plugins_loaded.
Returns
The priority. Default 10. Cast to int.

Parameters

$priority int
The default, 10.

Not usable from a theme, for the reason given under the_content_priority.

Example

// wp-content/mu-plugins/embed-gate-priority.php

add_filter(
	'calucon_embed_gate_render_block_priority',
	function ( $priority ) {
		// Let a block-rewriting plugin at 15 go
		// first.
		return 20;
	}
);

Fired at src/Integration/RenderBlock.php:38. Permalink

calucon_embed_gate_asset_version

Replace the version string on bundled assets.

apply_filters( 'calucon_embed_gate_asset_version', string $version, string $relative )

filter2 arguments

Fires
Once per bundled asset, when its ver argument is resolved.
Returns
The version string. Cast to string.

Parameters

$version string
The plugin version, or the file’s modification time on a site the plugin considers a development one.
$relative string
The asset path inside the plugin, for example assets/js/gate.js.

On a multi-server site the default modification time differs between machines, so one file can be served under several version strings. A build identifier is stable across servers.

Example

add_filter(
	'calucon_embed_gate_asset_version',
	function ( $version, $relative ) {
		// One deploy identifier for every
		// server behind the load balancer,
		// instead of each machine's own mtime.
		if ( defined( 'MY_DEPLOY_ID' ) ) {
			return (string) MY_DEPLOY_ID;
		}
		return $version;
	},
	10,
	2
);

Fired at src/Support/AssetVersion.php:67. Permalink


Actions

Return nothing. They run on every page view and must not write to the visitor’s browser.

calucon_embed_gate_before_render

Runs before a panel’s markup is built.

do_action( 'calucon_embed_gate_before_render', array $provider, array $ctx )

action2 arguments

Fires
Once per gated embed, before the wording hooks and before any markup exists.
Returns
Nothing.

Parameters

$provider array
The descriptor about to be rendered.
$ctx array
The integration context.

Runs for every panel on every page view. Callbacks must be inexpensive, and must not write to client storage.

Example

add_action(
	'calucon_embed_gate_before_render',
	function ( array $provider, array $ctx ) {
		// Remember which kinds of embed this
		// page holds, so the footer can enqueue
		// only the styles it needs.
		if ( ! isset( $GLOBALS['my_embed_kinds'] ) ) {
			$GLOBALS['my_embed_kinds'] = array();
		}
		$kind = '' !== $provider['kind']
			? $provider['kind']
			: 'generic';
		$GLOBALS['my_embed_kinds'][ $kind ] = true;
	},
	10,
	2
);

Fired at src/Plugin.php:446. Permalink

calucon_embed_gate_embed_gated

Runs once an embed has been gated.

do_action( 'calucon_embed_gate_embed_gated', array $provider, array $ctx )

action2 arguments

Fires
Once per gated embed, after its panel is rendered. The front-end assets are enqueued at the same point.
Returns
Nothing.

Parameters

$provider array
The descriptor that was gated.
$ctx array
The integration context.

Differs from before_render in timing: before_render runs as a panel begins to be built; this runs once the embed has been gated.

Example

add_action(
	'calucon_embed_gate_embed_gated',
	function ( array $provider, array $ctx ) {
		// A per-request tally, for a debug bar
		// or a footer note. Deliberately
		// in-memory: writing on a render would
		// make every page view a database
		// write.
		if ( ! isset( $GLOBALS['my_gated_embeds'] ) ) {
			$GLOBALS['my_gated_embeds'] = array();
		}
		$id = $provider['id'];
		$GLOBALS['my_gated_embeds'][ $id ] = 1 + (int) (
			isset( $GLOBALS['my_gated_embeds'][ $id ] )
				? $GLOBALS['my_gated_embeds'][ $id ]
				: 0
		);
	},
	10,
	2
);

Fired at src/Plugin.php:503. Permalink

calucon_embed_gate_flush_caches

Runs after the plugin clears its caches.

do_action( 'calucon_embed_gate_flush_caches' )

actionno arguments

Fires
After the plugin clears the caches it manages. Five occasions, not one: a settings save, the first settings save (a distinct event — a site that has never opened the screen has no option row, so WordPress adds one rather than updating it, and 1.0.0 was the release that started listening for it), activation, deactivation, and an update of the plugin.
Returns
Nothing. No arguments are passed.

Intended for caches the plugin cannot detect: a hosting layer, a CDN, a custom fragment cache. A stale page continues to serve panels built from superseded settings.

Do not assume a settings save: the same action arrives on activation and deactivation, when what is cached is a page with no gate at all, and after an update, when it is the previous version’s placeholder markup. A callback that purges is right in every case; one that reads the current settings to decide what to purge is not.

Example

add_action(
	'calucon_embed_gate_flush_caches',
	function () {
		// No arguments: add_action's default
		// $accepted_args of 1 is fine because
		// the callback declares none.
		if ( function_exists( 'my_host_purge_everything' ) ) {
			my_host_purge_everything();
		}
	}
);

Fired at src/Support/CacheFlush.php:54. Permalink


Hooks that can stop the gate

Effect. Each of these can cause a third party to be requested on page load, for every visitor, with no panel and no click. For those embeds the gate is absent, not weakened.

The Own hosts and Never gate settings are equivalent to two of these and remain visible in the admin.

calucon_embed_gate_should_gatecan disable the gate

Gate a URL, or allow it to load with the page.

apply_filters( 'calucon_embed_gate_should_gate', bool $gate, string $url, array $ctx )

filter3 arguments

Fires
Once per candidate embed, after a detection rule selects it and before rendering.
Returns
true to gate the embed, false to allow the request on page load. Cast to bool.

Parameters

$gate bool
What the gate intends to do. In practice always true here — the filter runs on embeds it has already decided to hold.
$url string
The URL that would be requested.
$ctx array
The integration context.

Blocks set to Always gate bypass this filter: force_gate is evaluated before the callback runs.

The Never gate setting expresses a host exemption in the admin, where it remains visible to the site owner.

Example

add_filter(
	'calucon_embed_gate_should_gate',
	function ( $gate, $url, array $ctx ) {
		// Tightening, not loosening: hold
		// everything that reaches a sidebar
		// widget, whatever the settings say.
		if ( 'widget' === $ctx['integration'] ) {
			return true;
		}
		return $gate;
	},
	10,
	3
);

Fired at src/Plugin.php:468. Permalink

calucon_embed_gate_is_own_hostcan disable the gate

Classify a host as the site itself.

apply_filters( 'calucon_embed_gate_is_own_host', bool $own, string $host )

filter2 arguments

Fires
Whenever a host is classified as this site or as a third party.
Returns
true if the host is the site itself, in which case its embeds are not gated. Cast to bool.

Parameters

$own bool
What the built-in rules concluded: the home and site URLs, since 1.0.0 the site’s asset bases as well — content_url(), includes_url(), plugins_url(), the uploads base and both theme directory URIs — the network’s other sites on multisite, and the configured own-hosts and never-gate lists.
$host string
The host being classified, lower-cased.

The always-gate list takes precedence and is evaluated first: hosts on it return false without reaching this filter.

Equivalent to the Own hosts setting, which requires no code.

This filter classifies a host, and 1.0.0 added one rule that classifies a URL instead: a <script> or <link rel=stylesheet> whose path contains /wp-content/ or /wp-includes/ is left alone whatever host serves it, for the CDN that rewrites the finished HTML rather than filtering WordPress’s URL functions. It does not run this filter, so returning false here will not gate such a URL. Two things do: the host belonging to a provider the plugin already knows, and the always-gate list. Never applies to iframes or images.

Example

add_filter(
	'calucon_embed_gate_is_own_host',
	function ( $own, $host ) {
		// This CDN serves this site's own media
		// library, so it is not a third party.
		// Anything added here is requested on
		// page load, with no panel and no
		// click.
		return $own || 'assets.my-cdn.example' === $host;
	},
	10,
	2
);

Fired at src/Plugin.php:413. Permalink

calucon_embed_gate_own_hostscan disable the gate

Add hosts treated as the site itself.

apply_filters( 'calucon_embed_gate_own_hosts', array $hosts )

filter1 argument

Fires
While the own-host list is assembled, before the detection pipeline is built.
Returns
The additional hosts. Cast to array and merged with the hosts the plugin derives itself, which are always present.

Parameters

$hosts array
The configured Own hosts and Never gate lists, merged. Both have the same effect here; they are separate settings because they mean different things to the site owner.

Hosts added here are not gated, and are requested on page load. The Own hosts setting is equivalent and remains visible to the site owner.

The list this filter receives is already populated. Since 1.0.0 it carries the host of every URL WordPress reports as the site’s own: home_url(), site_url(), content_url(), includes_url(), plugins_url(), the uploads base and both theme directory URIs — plus every domain on a multisite network. A CDN plugin that filters those functions therefore declares its host as the site’s own already, and needs nothing added here.

Those functions can only ever answer where do my assets live, so none of them can introduce a third party. This filter can: a host added here is exempt from gating on every page, for iframes as much as for scripts.

Example

add_filter(
	'calucon_embed_gate_own_hosts',
	function ( array $hosts ) {
		// Declared in code because it is
		// deploy-specific: the bucket name
		// differs between staging and
		// production.
		if ( defined( 'MY_MEDIA_HOST' ) ) {
			$hosts[] = MY_MEDIA_HOST;
		}
		return $hosts;
	}
);

Fired at src/Plugin.php:828. Permalink

calucon_embed_gate_www_equivalencecan disable the gate

Treat www. and the bare domain as one site.

apply_filters( 'calucon_embed_gate_www_equivalence', bool $equivalent )

filter1 argument

Fires
Once, when the host matcher is constructed.
Returns
true if www.example.com and example.com are the same site. Defaults to the www equivalence setting, which is enabled. Cast to bool.

Parameters

$equivalent bool
The configured value.

Disabling this changes classification in both directions: a host previously treated as the site becomes a third party, and on a site whose canonical host carries www the bare domain is no longer recognised as the site itself.

Example

add_filter(
	'calucon_embed_gate_www_equivalence',
	function ( $equivalent ) {
		// Only on a site where www and the bare
		// domain really are separate
		// properties. Check both spellings with
		// `wp calucon-embed-gate scan` after
		// changing this.
		return false;
	}
);

Fired at src/Plugin.php:405. Permalink


Settings

Option schema, ranges and enums.

One option, calucon_embed_gate_options, sanitised against a schema on every read. Malformed input falls back to defaults. Omitted keys revert to their defaults; there is no partial merge.

wp option get calucon_embed_gate_options --format=json
wp option patch update calucon_embed_gate_options detection images 1
wp option patch update calucon_embed_gate_options consent memory session
providers.{id}
enabled, privacy_variant (bool); note, action (strings, tags stripped, max 500 characters); privacy_url (https only, max 500). Keys must match ^[a-z0-9_-]{1,64}$.
custom_providers
Owner-defined rows: id (custom-<slug>, generated once), label (max 80), hosts, script_hosts, kind. Max 100 rows, 50 hosts each; wildcards are rejected here, and hosts a built-in already handles are stripped.
detection
iframes, scripts, images (images off by default), www_equivalence, output_buffer (bool); own_hosts, never_gate, always_gate — host lists where a leading *. wildcard is allowed. A pasted full URL is reduced to its host for you.
display
privacy_link (bool, off by default) — show each provider’s own privacy policy inside the panel.
appearance
Shape: preset (default|minimal|card), corners (''|square|rounded|pill|custom), radius (0–48), border_width (0–10, stored as a string), shadow, density, align.
Button: button_style, button_size, button_width, hover, play_icon, note_size, withdraw_style.
Poster: poster_panel, poster_dim.
Colour: bg, fg, accent, accent_fg, link, border_color, plus dark and dark_bg/dark_fg/dark_accent/dark_accent_fg applied only under prefers-color-scheme: dark.
consent
memory (off|session|persistent, default off), scope (embed|provider|all, default provider), duration_days (1–730, default 180).
cmp
bridge (bool, off), borlabs_group (slug, default external-media). An experimental tcf key existed before 1.0.0 and was removed with the IAB TCF bridge; a stored value is ignored.

Colour values take one of three forms only: a 3- or 6-digit hex such as #2271b1, the string preset:<slug> to follow a theme palette colour by name, or empty to inherit. Alpha hex, rgb() and named colours are rejected and fall back to inherit.

No other storage: no transients, no post meta, no user meta. Uninstalling deletes the option on every site of a network. Post content is never rewritten; gating happens at render time.

Authoritative schema: src/Support/Options.php, in defaults() and sanitize_tree(). Cache plugins are flushed automatically whenever this option changes.


WP-CLI

Two commands, both read-only.

Two subcommands under wp calucon-embed-gate. Both are read-only and make no outbound request; scan renders content in memory and reads the resulting markup.

wp calucon-embed-gate scan --format=json
wp calucon-embed-gate scan --posts=200
wp calucon-embed-gate providers --format=json
scan
Renders recent published posts and pages through the same pipeline the front end uses, and classifies every embed it finds. Columns: host, tag, label, status, count, first_seen, url.
--posts=<number>
How many recent posts and pages to render. Default 50, and clamped to 1–500 — a limit the command’s own help text does not mention.
--format=<format>
table (default), json, csv, yaml or count. Both subcommands accept it, and it is the only flag providers takes.
providers
Lists providers as the gate resolves them — built-ins, then anything registered through the filter, then the owner’s overrides. Columns: id, label, enabled, strategy, hosts, load_host, controller, privacy_url.

Every third-party row in scan output should read gated. The remaining statuses indicate an embed that loads without a click because a setting permits it: rule-disabled, provider-disabled, own-host. no-usable-url indicates markup with nothing to gate.

Widgets, template parts and page-builder layouts are outside the scan, as on the admin Status screen. scan runs the full the_content chain, so other plugins hooked there execute during it; the gate itself makes no request.


Withdrawal button

Clears remembered consent.

A shortcode and a block, rendering identical markup through the same code. Discards consent stored in the browser.

[calucon_embed_gate_withdraw]
[calucon_embed_gate_withdraw label="Forget my embed choices"]
label string
Button text. Defaults to a translated “Withdraw embed consents”.
Block equivalent
calucon-embed-gate/withdraw, with the same single label attribute. It is a dynamic block rendered on the server, so the two paths cannot drift apart.
Rendered markup
A <button type="button" class="cg-withdraw" data-cg-withdraw> plus a <span class="cg-withdraw__status" role="status" aria-live="polite">. The button’s aria-controls points at that span, whose id is generated per render — so never hard-code it.

Rendering it enqueues the front-end assets even on a page with no embeds, so it functions on a privacy policy page. It clears both storages unconditionally; with consent memory off nothing is stored and the control is inert.


Placeholder markup

The classes a theme may build on.

The shape the front-end script, the CMP bridge and the test suite depend on. Versioned as public API.

<div class="cg-embed" role="group" aria-label="..."
	data-cg-provider="youtube" data-cg-host="www.youtube.com"
	data-cg-payload="{...}" style="--cg-aspect:16/9">
	<div class="cg-embed__panel">
		<p class="cg-embed__note">...</p>
		<button type="button" class="cg-embed__button">...</button>
		<p class="cg-embed__fallback"><a href="..." rel="noopener nofollow">...</a></p>
		<p class="cg-embed__privacy"><a href="..." rel="noopener nofollow">...</a></p>
	</div>
</div>
.cg-embed
The container. Must carry data-cg-payload as well as the class — the script’s selector is both together.
.cg-embed__panel
Must be a <div> and a direct child of the container. The script checks the parent relationship, not just the class.
.cg-embed__button
A real <button type="button">. Not a link, not a div — this is the control the whole design rests on.
.cg-embed__fallback
A <p> wrapping the link that works without JavaScript. Find it by class, never by position. Since 0.10.0 a privacy link can follow it, so “the last link in the panel” is now the wrong one.
.cg-embed__privacy
Present only when the provider declares a policy URL and the site owner has enabled the link. Do not assume it exists.
.cg-embed--poster / .cg-embed__poster
Container modifier and image class when a poster is set. The image must keep alt="" and aria-hidden="true" — the group is already named — and the script removes it on activation by that class.
.cg-embed--silent
A companion script or stylesheet that belongs to another embed’s panel. Hidden, and activated together with the panel it belongs to. New in 0.11.0.
.cg-embed--active
Added by the script after activation. Not server-rendered.
.cg-embed__status, .cg-embed__error
Live regions created by the script when loading starts and when it fails. Also not server-rendered — do not put them in a template.

The panel is named with role="group" and an aria-label. Do not substitute a heading: the correct level cannot be determined from inside an embed, which may follow an h1 on a single post and an h2 on an archive. A named group supplies the name without altering the document outline.


Data attributes

Attributes the front end reads.
data-cg-provider
The provider id. Used for consent-memory keys, for matching silent companions to their panel, and for the button glyph.
data-cg-host
The host the embed will contact; omitted when unknown. It is what stops consenting to one unrecognised widget from loading a different one.
data-cg-payload
JSON the browser rebuilds the embed from. See below.
data-cg-activated
"1" once activation has begun; guards against activating twice. Removed again if loading fails.
data-cg-bridged
"1" when a consent platform activated this embed rather than the visitor. That distinction is what lets a withdrawal re-gate exactly those and leave real clicks alone.
data-cg-fallback
The fallback URL, stashed on the container before the panel is removed so the error state can still offer a link.
data-cg-withdraw
Marks the withdrawal control. Clicks are delegated from any ancestor carrying it.

Admin screens use further data-cg-* attributes. These are internal and may change without notice.

The payload

src string
The URL to load after consent. Absent for a srcdoc embed and for an inline loader.
attrs object
Attributes re-applied to the rebuilt element, filtered to a safelist: title, width, height, sandbox, loading, allow, allowfullscreen, referrerpolicy, type, id, name, class, data-secret, security, alt. autoplay is stripped from allow. In PHP this is an empty stdClass when there are no attributes.
srcdoc string
Present only for a srcdoc embed, carrying the original inline document to restore on click.
tag string
Present only when the element is not an iframe: embed, object, img or link.
strategy string
Present only when not iframe. script is the case the front end branches on.
inline string
The page’s own inline loader text, re-run after consent instead of fetching a URL. New in 0.11.0.

CSS custom properties

Seven properties that restyle the panel.

Set on .cg-embed to restyle the panel; no !important is required. The Appearance tab writes the same properties and includes a contrast check.

.cg-embed {
	--cg-bg:        #f6f7f7;
	--cg-fg:        #1d2327;
	--cg-accent:    #2271b1;
	--cg-accent-fg: #ffffff;  /* keep >= 4.5:1 against --cg-accent */
	--cg-radius:    8px;
	--cg-gap:       .75rem;
	--cg-font:      inherit;
}
--cg-bg
Panel background. Defaults to the theme’s base palette colour.
--cg-fg
Panel text, links and the focus outline. Defaults to the theme’s contrast.
--cg-accent
Button background and border.
--cg-accent-fg
Button text. It pairs with --cg-accent, not with --cg-bg — that is the pair to keep above 4.5:1.
--cg-radius
Corner radius of the container, poster and buttons. Default 4px.
--cg-gap
Panel padding and the gap between its parts. Default 0.75rem.
--cg-font
Font family on the container. Default inherit.
--cg-aspect
Set by the server, per embed, inline. It reserves the embed’s space so the page does not jump when it loads. Leave it alone.

The cg- prefix and the --cg-* and data-cg-* names were retained through the plugin’s rename; stylesheets written against earlier versions still apply.


Template variables

Variables passed to the template.

To replace the panel markup, copy the plugin’s template into the theme:

{your-theme}/calucon-embed-gate/placeholder.php
$provider, $ctx
As described above.
$aria_label
The accessible name for the panel.
$note, $action
Panel text and button text. Plain text — escape them.
$fallback_url, $fallback_label
The no-JavaScript link and its text.
$privacy_url, $privacy_label
The provider’s policy link, or empty when there is none or the site owner disabled it.
$payload_attr
The data-cg-payload value, already HTML-escaped — print it as-is.
$aspect
A CSS aspect ratio such as 16/9, or empty. Set it as --cg-aspect so the page does not reflow.
$host
The host the embed will contact, or empty.
$poster
A site-origin poster URL, or empty. Never substitute a provider-hosted image.

The template runs outside WordPress in the fixture suite, so it uses htmlspecialchars() rather than esc_attr(). A theme copy may use the WordPress functions.


Block attributes

The five attributes the editor adds.

The editor adds a panel to core/embed, core/html, core/video and core/audio. core/html is included because pasted iframe markup lands there.

caluconEmbedGate string
Empty for the site default, always, or never. always sets force_gate and skips should_gate.
caluconEmbedGatePoster number
An attachment id. The server resolves it and returns nothing unless the resulting URL is on this site — so a CDN-offloaded media library needs that host declared under Own hosts.
caluconEmbedGatePosterUrl string
The inspector’s preview only. The id above is what the server renders from.
caluconEmbedGateAction string
Per-block button text, capped at 120 characters.
caluconEmbedGateNote string
Per-block notice text, capped at 400 characters.

These caps differ from the 500-character cap on the equivalent per-provider settings. Values travel as block attributes; nothing is stored in post meta.


Front-end JavaScript

Ready hooks, bridge, consent storage.

No custom event is dispatched. Activation fires nothing a script can listen for. The available surface is a hook map and a bridge object.

window.caluconEmbedGateReadyHooks

Some provider SDKs render only the placeholders present when they run, so an embed injected after consent must be re-scanned. The plugin ships hooks for Strava, Twitter, Instagram and Facebook. A hook registered under an existing id replaces the built-in.

window.caluconEmbedGateReadyHooks =
	window.caluconEmbedGateReadyHooks || {};

// Keyed by provider id. Runs after the SDK loads, and again after
// each later activation of the same provider. Registration order
// does not matter: before or after gate.js are both fine.
window.caluconEmbedGateReadyHooks['example-videos'] = function () {
	if ( window.ExampleVideos && window.ExampleVideos.scan ) {
		window.ExampleVideos.scan();
	}
};

Each hook is wrapped in try/catch; a failing hook does not halt the page.

window.caluconEmbedGateBridge

The interface the consent-platform bridge is built on. each() walks the gated panels, grant() activates one, grantAll() activates all, and regate() re-gates those the bridge opened — identified by data-cg-bridged — and not those a visitor clicked.

Intended only for integrating a consent platform the plugin does not detect. grantAll() on page load would load every embed for every visitor.

Consent memory

Off by default. When enabled the browser stores one key; the server is not informed, as a server-side consent state would make every page uncacheable.

Storage key
calucon-embed-gate, in sessionStorage when memory is set to session and localStorage otherwise.
Value
{"v":1,"g":{…}} — a schema version and a map of grant keys to timestamps.
Grant keys
* for scope all; p:<provider> for scope provider, with @<host> appended for generic providers so one unknown widget does not unlock another; e:<url> for scope embed. No identifier of any kind, only what was consented to.
Expiry
Checked when read, not on a timer. Session memory lasts the session.
Writes
Only on a real click. Page-load code reads storage; it never writes. Restoring from memory and bridge grants both explicitly skip the write.

Constants

Three defined constants.
CALUCON_EMBED_GATE_VERSION
The plugin version string.
CALUCON_EMBED_GATE_FILE
Absolute path to the main plugin file. Use it with plugins_url() to build an asset URL.
CALUCON_EMBED_GATE_DIR
Absolute path to the plugin directory.

Those three only. There is no CALUCON_EMBED_GATE_URL; URLs are derived at each call site from CALUCON_EMBED_GATE_FILE via plugins_url().


What is promised

Semver coverage.

The plugin follows semantic versioning, and 1.0.0 turned the coverage into a written promise, pinned by a test in the plugin’s own suite (StabilityContractTest). Stable across minor releases:

  • The placeholder markup contract — the classes, roles, data-cg-* attributes and --cg-* custom properties described above.
  • The filters and actions on this page.
  • The template variables handed to a placeholder override.
  • The settings keys under calucon_embed_gate_options.
  • The WP-CLI commands and their output shape.

Deliberately not promised, because they are data rather than API: the provider descriptors (hosts, endpoints, labels) and the tested-platform lists on the Compatibility screen. Both may change in a minor release. 1.0.0 also declares the plugin feature-complete: from here the roadmap is fixes, field-validation findings and WordPress/PHP compatibility, not new surface.

The remainder is documented as observed behaviour, not as a contract. caluconEmbedGateReadyHooks, caluconEmbedGateBridge, the browser-storage format and the data-cg-payload key set appear in no plugin document; the consent-platform bridge depends on them. Pin a version if your code does.

Neither of the following is public API. Plugin::instance() does not exist: the constructor is private and no accessor is provided. PipelineFactory is part of the test suite, which is excluded from the built plugin.


What no hook can do

  • Make the plugin fetch something remote. There is no supported way and there will not be. Do not fetch a thumbnail, a poster or a favicon from the provider in a filter either — that is the request the panel exists to prevent, just moved to the server. Posters must come from your own site, which the server enforces.
  • Reinstate autoplay. It is stripped from allow after every filter has run. It is also a WCAG 1.4.2 failure, and not what someone who pressed a button asked for.
  • Store anything before the click. No cookie, no localStorage, no sessionStorage — not even to remember something helpful.
  • Reorder the gate past the content filters. It runs late on purpose so it sees the output of shortcodes and blocks. Page-builder output that bypasses those filters needs the Gate the whole page output setting, not custom code.

The gate fails closed: an unrecognised third-party iframe is caught by the generic rule even when a descriptor is wrong. A broken customization renders a generic panel rather than permitting the request.


Elsewhere

  • Plugin overview — what it does and why.
  • Customizing guide — the same ground in prose, with the reasoning rather than the signatures.
  • docs/customizing.md inside the plugin folder — the same reference, shipped in the zip, so an agent working in your codebase can read it without a network.
  • Source on GitHub — issues, and private security reporting. Any way to make a page contact a third party before the click counts as a vulnerability.
  • Live demo — every provider on one page, with nothing loading until you press a button.
  • WordPress.org — install, changelog, support forum.

Generated from the plugin source; the hook list is verified against the code on every build, in both directions. Describes technical behaviour only and makes no claim regarding legal obligations.