Many “heavyweight” compliance plugins bloat your site and charge monthly fees for simple functionality. This tutorial shows you how to build a custom, lightweight WordPress plugin that handles Google Consent Mode v2 signals using cookies and vanilla JavaScript.
The Core Concept: Timing is Everything
The most common mistake in GCMv2 implementation is applying consent defaults after the Google Tag Manager (GTM) script has already started loading. If GTM fires before your “Deny” signal, Google cookies will be set regardless of your settings.
Step 1: Plugin Structure
Create a folder named my-consent-plugin in your /wp-content/plugins/ directory.
Plaintext
my-consent-plugin/
├── my-consent-plugin.php # Plugin logic & WordPress hooks
└── assets/
├── consent.js # Logic for cookies and UI interactions
└── style.css # (Optional) For modal styling
Step 2: The Plugin Header and Early Initialisation
The key to fixing “early cookie” bugs is using the wp_head hook with a priority of 0. This ensures your default consent state is declared before any tracking scripts.
my-consent-plugin.php
PHP
<?php
/**
* Plugin Name: My Consent Plugin (Lightweight GCMv2)
* Description: Bottom-right consent launcher + modal with cookie persistence.
* Version: 1.0.0
*/
if (!defined('ABSPATH')) exit;
// 1. Output default consent state IMMEDIATELY in <head>
add_action('wp_head', function () {
?>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Check for existing consent cookie before setting defaults
var consentCookie = document.cookie.match(/^(.*;)?\s*my_consent_v2\s*=\s*([^;]+)(.*)?$/);
var savedConsent = consentCookie ? JSON.parse(decodeURIComponent(consentCookie[2])) : null;
gtag('consent', 'default', savedConsent || {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'wait_for_update': 500
});
</script>
<?php
}, 0);
// 2. Enqueue the Front-end Logic
add_action('wp_enqueue_scripts', function () {
wp_enqueue_script('my-consent-js', plugins_url('assets/consent.js', __FILE__), array(), '1.0.0', true);
wp_localize_script('my-consent-js', 'MyConsentCfg', array(
'cookieName' => 'my_consent_v2',
'cookieDays' => 180,
));
});
// 3. Simple UI Markup
add_action('wp_footer', function () {
?>
<div id="consent-launcher" class="my-consent-launcher">Cookie Settings</div>
<div id="consent-modal" class="my-consent-modal" hidden>
<div class="modal-content">
<h3>Privacy Preferences</h3>
<label><input type="checkbox" data-consent="analytics_storage"> Analytics</label>
<label><input type="checkbox" data-consent="ad_storage"> Marketing</label>
<div class="actions">
<button data-action="deny">Deny All</button>
<button data-action="accept">Accept All</button>
<button data-action="save">Save Preferences</button>
</div>
</div>
</div>
<?php
});
Step 3: Handling State and Cookies
Our JavaScript needs to bridge the gap between the UI and the gtag command. We use cookies rather than localStorage to ensure better compatibility and server-side readability if needed.
assets/consent.js
Inside your JS file, focus on the gtag('consent', 'update', ...) command. This is what tells Google to “unblock” tags once a user clicks Accept.
Key Logic: When the page loads, if a cookie exists, we must fire an
updateimmediately so GTM knows the user’s previous choice.
JavaScript
(function () {
const cfg = window.MyConsentCfg || { cookieName: "my_consent_v2", cookieDays: 180 };
const modal = document.getElementById("consent-modal");
const launcher = document.getElementById("consent-launcher");
function setConsent(consentObj) {
const expiry = new Date(Date.now() + cfg.cookieDays * 864e5).toUTCString();
document.cookie = `${cfg.cookieName}=${encodeURIComponent(JSON.stringify(consentObj))}; expires=${expiry}; path=/; SameSite=Lax`;
window.gtag("consent", "update", consentObj);
modal.hidden = true;
}
// Event Listeners for UI
launcher.onclick = () => modal.hidden = false;
modal.addEventListener('click', (e) => {
const action = e.target.dataset.action;
if (action === 'accept') {
setConsent({
analytics_storage: 'granted',
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted'
});
} else if (action === 'deny') {
setConsent({
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied'
});
}
});
})();
Step 4: Verification (The “Reality Check”)
Don’t trust the UI. Verify using these two methods:
1. Google Tag Assistant
Run your site through Tag Assistant.
- Success: You should see a “Consent” tab. On page load, it should show Default (Denied). After clicking Accept, a new entry should appear: Update (Granted).
2. The Browser “Network” Test
This is the most critical step.
- Clear your cookies and refresh the page.
- Do not click the banner.
- Check the “Application” or “Storage” tab in DevTools.
- Verification: If you see
_gaor_gidcookies before clicking “Accept,” your load order is wrong. Yourwp_headscript must fire earlier.
Common Pitfalls to Avoid
- Caching: If you use a caching plugin or Cloudflare, ensure your early
<script>tag isn’t being deferred or “optimised” to the footer. - Hardcoded Tags: If your theme has a hardcoded
gtag.jsscript, it might bypass your plugin settings. Always route tags through GTM for best control. - Regional Settings: GCMv2 often requires different defaults for EU vs. US users. You can extend the PHP logic to check for user regions if necessary.