How to implement GDPR consent for tracking in JS and PHP in WordPress

Contents

Introduction

This article is a complete, practical tutorial for implementing GDPR-compliant consent for tracking in a WordPress site using JavaScript and PHP. It explains legal principles briefly, shows recommended consent categories, provides front-end cookie banner and consent manager implementations, explains how to block and conditionally load trackers, and shows secure server-side checks and optional logging. All example code is ready to paste into theme/plugin files or standalone assets.

GDPR essentials (brief, practical)

Before implementation, remember the core GDPR requirements relevant to tracking:

  • Prior consent — non-essential cookies and tracking are not placed or fired before the user gives informed, explicit consent.
  • Granularity — you must allow users to accept or refuse categories (analytics, advertising, functional, etc.), not just an “accept all” button.
  • Withdraw change — users must be able to change or withdraw consent easily at any time.
  • Record keeping — keep a record of consent for accountability (time, categories, version of policy).
  • Data minimisation privacy-friendly defaults — minimize data collection and enable IP anonymisation where possible.

Design: consent categories, storage, and enforcement

Design your consent system with:

  • Categories (example): necessary, preferences, analytics, advertising. Necessary cookies are always allowed others require explicit consent.
  • Storage format: store a JSON object in a cookie (or localStorage) describing the consent choices, timestamp, and version. Optionally sign the cookie with a server-side HMAC to prevent tampering.
  • Enforcement methods:
    1. Client-side blocking: mark tracking scripts with a safe type (e.g., type=text/plain or data-consent attributes). On consent, replace them with executable scripts or load them dynamically.
    2. Server-side gating: in PHP, read the consent cookie and only print tracking/snippet code when consent is present for the category. Use both client and server checks for defense in depth.

High-level implementation steps

  1. Decide categories and text for cookie banner/policy.
  2. Build the cookie banner and consent modal UI (HTML CSS JS).
  3. Implement a robust JS consent manager: read/write cookie, show modal, load trackers when consented.
  4. Mark tracking scripts so they don’t run before consent (use data attributes or non-executable script types), and enable dynamic loading after consent.
  5. Implement PHP helpers that read consent cookie securely and gate server-side output (wp_head/wp_footer, plugin integrations).
  6. Log or store consent for audits (optional): user meta for logged-in users or a database table for anonymous users.
  7. Test thoroughly (no trackers before consent correct loading after withdraw consent works).

Cookie categories reference table

Category Purpose Typical trackers Default behaviour
necessary Site functionality (login, cart) Session cookies Always allowed
preferences UI preferences (language) Custom scripts Require consent
analytics Usage measurement Google Analytics, Matomo Require consent
advertising Ad personalisation GTM tags, FB Pixel, ad networks Require consent

Front-end: cookie banner, consent modal and consent manager JS

Below is a comprehensive example of a cookie banner, modal, JS consent manager, and helper to dynamically enable scripts. Place the HTML for the banner in a theme partial (footer or wp_footer). The CSS should go in your theme stylesheet. JS should be enqueued as a separate file and executed early (but not blocking). Examples included show the core logic.

Cookie banner HTML (example)






CSS (basic styles — put in style.css)

/ Minimal styling for banner and modal /
#cookie-banner { position: fixed left: 0 right: 0 bottom: 0 background:#222 color: #fff padding: 12px z-index: 9999 }
#cookie-banner a { color: #9cf }
#cookie-modal { position: fixed top: 8% left: 50% transform: translateX(-50%) background: #fff color: #000 padding: 20px border: 1px solid #ddd z-index: 10000 display: none }

JavaScript consent manager (core logic)

This script does the heavy lifting: it stores the consent cookie (signed optionally by the server if desired), reads it, shows the banner when needed, and loads trackers protected by data-consent attributes. It also updates Google Consent Mode and logs a consent record using a server endpoint (optional).

/ cookie-consent.js — load early (deferred=false) /
/ Configuration /
const COOKIE_NAME = wp_cookie_consent
const COOKIE_DAYS = 365
const COOKIE_PATH = /
const CONSENT_VERSION = 1.0

/ default state: necessary is implicitly true /
const DEFAULT_CONSENT = {
  necessary: true,
  preferences: false,
  analytics: false,
  advertising: false,
  version: CONSENT_VERSION,
  timestamp: null
}

function setCookie(name, value, days) {
  const expires = new Date(Date.now()   days  864e5).toUTCString()
  document.cookie = {name}={encodeURIComponent(value)} expires={expires} path={COOKIE_PATH} SameSite=Lax Secure
}

function getCookie(name) {
  const r = document.cookie.match((^)s   name   s=s([^] ))
  return r ? decodeURIComponent(r.pop()) : null
}

function eraseCookie(name) {
  document.cookie = name   = Max-Age=0 path=   COOKIE_PATH
}

function readConsent() {
  const raw = getCookie(COOKIE_NAME)
  if (!raw) return null
  try {
    return JSON.parse(raw)
  } catch (e) {
    console.warn(Consent cookie parse error, e)
    return null
  }
}

function saveConsent(consent) {
  consent.timestamp = new Date().toISOString()
  consent.version = CONSENT_VERSION
  const serialized = JSON.stringify(consent)
  setCookie(COOKIE_NAME, serialized, COOKIE_DAYS)
  // Optional: notify server for logging, passing serialized and (if implemented) signature
  // fetch(/wp-json/consent/v1/log, { method: POST, credentials:same-origin, headers:{Content-Type:application/json}, body:serialized })
  return consent
}

/ UI helpers /
function showBanner() {
  const banner = document.getElementById(cookie-banner)
  if (banner) {
    banner.style.display = block
    banner.setAttribute(aria-hidden, false)
  }
}

function hideBanner() {
  const banner = document.getElementById(cookie-banner)
  if (banner) {
    banner.style.display = none
    banner.setAttribute(aria-hidden, true)
  }
}

function showModal() {
  const modal = document.getElementById(cookie-modal)
  if (modal) {
    modal.style.display = block
    modal.setAttribute(aria-hidden, false)
  }
}

function hideModal() {
  const modal = document.getElementById(cookie-modal)
  if (modal) {
    modal.style.display = none
    modal.setAttribute(aria-hidden, true)
  }
}

/ Script loader: find scripts with data-consent and load/activate them when allowed /
function activateConsentCategory(category) {
  // Update Google Consent Mode (if present)
  if (window.gtag  typeof window.gtag === function) {
    if (category === analytics) {
      // Example: set analytics_storage to granted
      window.gtag(consent, update, { analytics_storage: granted })
    }
    if (category === advertising) {
      window.gtag(consent, update, { ad_storage: granted })
    }
  }

  // Load all script placeholders for this category
  const nodes = document.querySelectorAll(script[data-consent=   category   ])
  nodes.forEach((node) => {
    // if script already activated, skip
    if (node.getAttribute(data-activated) === 1) return

    const newScript = document.createElement(script)
    // Copy attributes
    if (node.src) {
      newScript.src = node.src
    } else {
      newScript.textContent = node.textContent
    }
    // Copy async/defer if present on original
    if (node.hasAttribute(async)) newScript.async = true
    if (node.hasAttribute(defer)) newScript.defer = true

    newScript.setAttribute(data-activated-by, consent)
    node.parentNode.insertBefore(newScript, node)
    node.setAttribute(data-activated, 1)
  })
}

/ Deactivate / remove cookies and trackers for category withdrawal /
function withdrawConsentCategory(category) {
  // For analytics: send update to consent mode
  if (window.gtag  typeof window.gtag === function) {
    if (category === analytics) {
      window.gtag(consent, update, { analytics_storage: denied })
    }
    if (category === advertising) {
      window.gtag(consent, update, { ad_storage: denied })
    }
  }
  // Optionally remove cookies set by trackers (cant always remove third-party cookies)
  if (category === analytics) {
    // common GA cookies
    eraseCookie(_ga)
    eraseCookie(_gid)
    eraseCookie(_gat)
  }
  // More removal logic depends on specific trackers
}

/ Apply consent: load allowed categories and keep necessary active /
function applyConsent(consent) {
  if (!consent) return
  // Always necessary
  // Activate categories opted-in
  [preferences, analytics, advertising].forEach((cat) => {
    if (consent[cat]) {
      activateConsentCategory(cat)
    } else {
      withdrawConsentCategory(cat)
    }
  })
}

/ Initialize UI listeners /
document.addEventListener(DOMContentLoaded, function () {
  const stored = readConsent()
  if (!stored) {
    showBanner()
  } else {
    applyConsent(stored)
  }

  // Banner buttons
  const acceptAll = document.getElementById(cookie-accept-all)
  const reject = document.getElementById(cookie-reject)
  const openSettings = document.getElementById(cookie-open-settings)

  if (acceptAll) acceptAll.addEventListener(click, function () {
    const consent = Object.assign({}, DEFAULT_CONSENT, { preferences: true, analytics: true, advertising: true })
    saveConsent(consent)
    applyConsent(consent)
    hideBanner()
  })

  if (reject) reject.addEventListener(click, function () {
    const consent = Object.assign({}, DEFAULT_CONSENT, { preferences: false, analytics: false, advertising: false })
    saveConsent(consent)
    applyConsent(consent)
    hideBanner()
  })

  if (openSettings) openSettings.addEventListener(click, function () {
    showModal()
  })

  // Modal: save/cancel
  const saveBtn = document.getElementById(cookie-save)
  const cancelBtn = document.getElementById(cookie-cancel)

  if (saveBtn) saveBtn.addEventListener(click, function () {
    const form = document.getElementById(cookie-consent-form)
    const formData = new FormData(form)
    const consent = {
      necessary: true,
      preferences: formData.get(preferences) === on  formData.get(preferences) === true,
      analytics: formData.get(analytics) === on  formData.get(analytics) === true,
      advertising: formData.get(advertising) === on  formData.get(advertising) === true
    }
    saveConsent(consent)
    applyConsent(consent)
    hideModal()
    hideBanner()
  })

  if (cancelBtn) cancelBtn.addEventListener(click, function () {
    hideModal()
  })

  // Provide a way to reopen settings (could be a preference link in footer)
})

How to mark tracker scripts so they don’t run until consent

When adding third-party scripts directly into HTML or via wp_enqueue_script, mark them with a data-consent attribute or a non-executable type so they won’t run until the consent manager activates them.








These script elements wont be executed until the consent manager clones them into real script tags (see activateConsentCategory in the JS above).

Server-side: PHP helpers and conditional printing in WordPress

Client-side blocking is necessary but not sufficient in all contexts. It’s good practice to have server-side gating for tracking snippets that may be injected by the theme or plugins. The following PHP snippets show: a) reading the consent cookie safely, b) gating code printed in wp_head or wp_footer, and c) optionally signing/validating the cookie. Place the PHP in your themes functions.php or a small plugin.

1) Simple helper to read consent cookie


2) Optional: sign consent cookie to detect tampering

You can sign the cookie value using an HMAC with a secret key (use a site secret like AUTH_KEY). When saving JS-side, you can first request the server to sign or implement signing server-side when a POST saves consent. Below: PHP function to create and validate an HMAC signature for a consent payload.

 POST,
        callback => consent_save_handler,
        permission_callback => __return_true,
    ))
})

function consent_save_handler(WP_REST_Request request) {
    body = request->get_body()
    // validate JSON
    data = json_decode( body, true )
    if ( json_last_error() !== JSON_ERROR_NONE ) {
        return new WP_Error(invalid, Invalid JSON, array(status => 400))
    }
    // compute signature
    secret = defined(COOKIE_CONSENT_SECRET) ? COOKIE_CONSENT_SECRET : AUTH_KEY
    sig = hash_hmac(sha256, body, secret)
    payload = base64_encode( body ) . . . sig
    // set cookie server-side for stronger security (SameSite/Secure)
    setcookie(wp_cookie_consent, payload, time()   365  DAY_IN_SECONDS, /, , is_ssl(), true)
    // optional: store server-side record for audit (be mindful of PII)
    return rest_ensure_response(array(ok => true))
}

// To read and validate:
function wp_get_signed_cookie_consent() {
    if ( empty( _COOKIE[wp_cookie_consent] ) ) return null
    val = wp_unslash( _COOKIE[wp_cookie_consent] )
    if ( false === strpos( val, . ) ) return null
    list(b64, sig) = explode(., val, 2)
    json = base64_decode( b64 )
    if ( ! json ) return null
    secret = defined(COOKIE_CONSENT_SECRET) ? COOKIE_CONSENT_SECRET : AUTH_KEY
    expected = hash_hmac(sha256, json, secret)
    if ( ! hash_equals( expected, sig ) ) {
        // signature mismatch
        return null
    }
    data = json_decode( json, true )
    return data
}
?>

3) Gating tracking code in wp_head/wp_footer

Use the helper to decide whether to print a tracking snippet server-side. This is especially useful for server-rendered tags or if a plugin adds analytics. For example, printing GA only when analytics consent is granted:

 do not print GA server-side
    }
    // print GA script (minimal demo)
    ?>
    
    
    

4) Enqueue client JS and localize variables

Enqueue the consent manager JS via wp_enqueue_script. Use wp_localize_script or wp_add_inline_script to pass server data (e.g., current consent state, whether to sign). This helps server-client discussion and is safer than embedding cookie reading logic in many places.

 consent,
        cookie_name => wp_cookie_consent,
    ))
}
?>

Advanced integrations

Google Tag Manager and Google Consent Mode

For GTM you can:

  • Use Google Consent Mode: update consent state via gtag(consent,update, { analytics_storage: granteddenied, ad_storage: granteddenied }) when consent changes.
  • Load GTM only after necessary consent or use a minimal GTM snippet to only configure consent mode and not load tags until consent is granted.
/ Example: after user grants analytics and/or advertising /
if (consent.analytics) {
  gtag(consent, update, { analytics_storage: granted })
}
if (consent.advertising) {
  gtag(consent, update, { ad_storage: granted })
}
if (!consent.analytics) {
  gtag(consent, update, { analytics_storage: denied })
}

Facebook Pixel and other advertising trackers

Follow the same approach: place tags with data-consent=advertising and only activate them after advertising consent. Also remove cookies where possible when consent is withdrawn.

Logging consent for audits (optional)

For accountability, log who gave which consent, when and which version of the policy they accepted. For logged-in users, add a user meta record when they save consent. For anonymous users, write a minimal record to a custom DB table or to a secure logging service. Be careful to avoid storing PII unnecessarily.

 consent,
            timestamp => current_time(mysql),
            ip => _SERVER[REMOTE_ADDR], // consider hashing IP or avoid storing raw IP
        ) )
    } else {
        global wpdb
        table = wpdb->prefix . consent_log
        // create table on plugin activation — omitted here
        wpdb->insert( table, array(
            consent => wp_json_encode( consent ),
            created => current_time(mysql),
            ip_hash => hash(sha256, _SERVER[REMOTE_ADDR] . AUTH_KEY),
        ) )
    }
}
?>

Testing checklist

  1. Open an incognito window: verify no analytics, ad pixels, or requests to trackers until you accept them.
  2. Accept only analytics: check network tab shows GA but not advertising endpoints.
  3. Withdraw consent: verify trackers stop and cookies are removed where possible.
  4. Check server-side gating: disable JS and verify server did not print tracking code when consent absent.
  5. Test with different browsers and a consent cookie tampered with — signed cookie should be rejected if signature invalid.

Best practices and additional considerations

  • Keep the consent modal brief and user-friendly. Provide a link to a full cookie policy.
  • Make the necessary category truly necessary — dont include analytics there.
  • Minimize data collection and anonymize IPs for analytics (e.g., gtag anon).
  • Use secure cookie attributes: Secure, HttpOnly where appropriate (HttpOnly cant be accessed by JS though), and SameSite=Lax for standard consent cookies.
  • Record consent events server-side for audit only as needed avoid storing PII unnecessarily. Consider hashing IPs for compliance.
  • Keep a version number in your consent data and update version when policy changes to force re-consent when necessary.
  • Be careful with third-party plugins that inject trackers — check their output and add server-side gating if necessary.

Summary

Implementing GDPR consent for tracking in WordPress requires both client-side and server-side measures. Use a clear banner and granular modal, store consent as a structured cookie (optionally signed), block trackers until consent is given (using data-consent attributes or server-side gating), and log consent for accountability. Test thoroughly for edge cases such as cookie tampering and consent withdrawal.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *