Build an add-on
Wire any third-party technology into the Kjeks consent layer, using the Google Consent Mode add-on (kjeks-google) as the worked example.
A Kjeks add-on is just a normal WordPress plugin that declares its technologies to the core plugin. Kjeks then holds those scripts back until the visitor grants the matching consent category. You never write the blocking yourself — you describe what runs and which category it belongs to. Everything below maps to real code in kjeks-google.
1 · Anatomy of an add-on
Plugin header
Declare a dependency on the core plugin so WordPress itself warns when it's missing, and mark it network-capable:
<?php
/**
* Plugin Name: Kjeks Google
* Description: Google Tag Manager / GA4 for Kjeks via Consent Mode v2.
* Requires at least: 6.8
* Requires PHP: 8.3
* Requires Plugins: kjeks // WordPress warns if Kjeks is missing
* Network: true // multisite-friendly, like the core plugin
*/
Boot only when Kjeks is present
The Requires Plugins: kjeks header already stops WordPress from
activating your add-on without Kjeks (and from deactivating Kjeks while yours is
active). So you don't need a dependency class — a one-line
function_exists() guard on the core plugin's public API is enough
insurance for the rare “active but not loaded” edge:
add_action(
'plugins_loaded',
static function (): void {
// The Requires Plugins header prevents activation without Kjeks; this
// one-liner is cheap insurance for the "active but API not loaded" edge.
if ( ! function_exists( 'kjeks_register_integration' ) ) {
return;
}
Plugin::instance()->boot();
}
);
kjeks_register_integrations action, which
only the core plugin fires — so even without the guard you can't
fatal by calling kjeks_register_integration() when Kjeks is absent.
Kjeks loads its API on init (priority 20 fires that hook), and booting
on plugins_loaded keeps the ordering correct.
2 · Register your technology
This is the heart of every add-on. Hook the
kjeks_register_integrations action and call
kjeks_register_integration( $id, $args ). Kjeks blocks the scripts
until the category is granted:
add_action( 'kjeks_register_integrations', function (): void {
kjeks_register_integration(
'google-tags', // unique integration id
array(
'category' => 'analytics', // gate: runs only when this is granted
'label' => 'Google Tag Manager / Analytics',
'src_scripts' => array(
'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX',
),
'inline' => array(
"gtag('js', new Date()); gtag('config', 'G-XXXXXXX');",
),
// 'handles' => array( 'my-registered-handle' ), // or gate an already-enqueued script
)
);
} );
The arguments
| Key | What it does |
|---|---|
category | The gate: preferences, analytics, or marketing. Unknown or necessary falls back to marketing. |
label | Human name shown in the cookie declaration. |
src_scripts | External scripts to inject after consent (a URL string, or { src, attrs }). |
inline | Inline JS snippets to run after consent. |
handles | Registered script handles to hold back — gate a script another plugin/theme enqueued. |
kjeks_enqueue_script(),
kjeks_add_inline_script(), and kjeks_embed() — see the
core 201 guide.
3 · React to consent on the front end
Registration is enough for most add-ons — Kjeks loads your scripts when consent
is granted. But some technologies (like Google Consent Mode) must also
signal on every change. Kjeks gives you a small JS surface:
window.kjeks.isGranted( category ) and the
kjeks:granted / kjeks:withdrawn events.
// Consent Mode v2 defaults — printed in wp_head BEFORE anything Google loads.
gtag( 'consent', 'default', {
ad_storage: 'denied', ad_user_data: 'denied',
ad_personalization: 'denied', analytics_storage: 'denied',
wait_for_update: 500,
} );
// Sync to Kjeks now and on every change.
function granted( c ) {
return window.kjeks ? window.kjeks.isGranted( c ) : false;
}
function apply() {
if ( typeof gtag !== 'function' ) return;
gtag( 'consent', 'update', {
analytics_storage: granted( 'analytics' ) ? 'granted' : 'denied',
ad_storage: granted( 'marketing' ) ? 'granted' : 'denied',
} );
}
window.addEventListener( 'kjeks:granted', apply );
window.addEventListener( 'kjeks:withdrawn', apply );
document.addEventListener( 'DOMContentLoaded', apply );
kjeks-google prints the first block (defaults denied) in wp_head at
priority 1, and the sync block at priority 2 — so Google's own state is
denied until Kjeks says otherwise, a second layer on top of the gate.
4 · Settings, config & updates
Store configuration with AddonKit
The core plugin ships a small set of shared base classes under
Soderlind\Kjeks\AddonKit so add-ons don't re-implement settings
plumbing. Extend AddonKit\SettingsPage and implement a handful of
methods — the base handles reading/writing the option (network vs regular),
nonces, and mounting your screen as a submenu of the core “Cookie
Consent” menu on both single-site and Multisite. The
kjeks-scripting add-on is the reference example:
use Soderlind\Kjeks\AddonKit\Categories;
use Soderlind\Kjeks\AddonKit\SettingsPage;
final class Settings extends SettingsPage {
protected function option_key(): string { return 'kjeks_scripting'; }
protected function menu_slug(): string { return 'kjeks-scripting'; }
protected function page_title(): string { return __( 'Kjeks Scripting', 'kjeks-scripting' ); }
protected function menu_title(): string { return __( 'Scripting', 'kjeks-scripting' ); }
protected function defaults(): array { return ScriptRules::defaults(); }
protected function normalize( array $raw ): array { return ScriptRules::normalize( $raw ); }
protected function render_fields( string $prefix, array $config ): void {
// Echo the form rows. Build field names with $this->field_name( $prefix, ... )
// so they work on both the site and network forms, and render the category
// picker with Categories::render_select( $name, $id, $current ).
}
}
| Class | Role |
|---|---|
AddonKit\Plugin | Singleton bootstrap — extend it and implement register() to wire your hooks. |
AddonKit\SettingsPage | Abstract settings screen that mounts under the core menu. Implement option_key(), menu_slug(), page_title(), defaults(), normalize(), render_fields() (plus optional menu_title()/parent_slug()). |
AddonKit\Options | Reads/writes the option, network-aware. |
AddonKit\Categories | Consent-category picker: choices(), is_valid(), coerce(), render_select(). |
class_exists( \Soderlind\Kjeks\AddonKit\Plugin::class ) before
extending. In your add-on's own tests, don't require the core
vendor/autoload.php — register a targeted
spl_autoload_register for the AddonKit namespace instead.
Offer a filter
Let other code adjust your resolved config at runtime:
add_filter( 'kjeks_google_config', function ( $config, $blog_id ) {
// $config = [ 'gtm_id' => '', 'ga4_id' => 'G-XXXX', 'gating_category' => 'analytics' ]
return $config;
}, 10, 2 );
Ship self-updates (optional)
Add soderlind/wordpress-github-updater via Composer and call
GitHubUpdater::init() in your main file, exactly like the rest of the
family — see any add-on's bootstrap for the pattern.
Requires Plugins: kjeks header · ②
one-line function_exists() / class_exists() guard · ③
register on kjeks_register_integrations · ④
(if needed) sync via kjeks:granted/kjeks:withdrawn · ⑤
settings via AddonKit\SettingsPage + a config filter · ⑥ verify with
the scanner that nothing loads before consent.
Study the full manual example in the kjeks-google repository, the AddonKit example in kjeks-scripting, and see the core developer guide for the complete integration API.