Documentation

M Cookie Consent

M Cookie Consent is a GDPR consent banner for WordPress that keeps Google Analytics 4, Google Tag Manager, the Meta Pixel and your own head scripts blocked until the visitor agrees to the matching category. It implements Google Consent Mode v2 without any external service, account or API key: the decision is made in the visitor's browser and stored in a single first-party cookie, so full-page caching keeps working. This manual is written for two people: the administrator who has just activated the plugin, and whoever has to undo a change that went wrong. Every claim in it was checked against the source of version 1.0.1.

Written for version 1.1.2. WordPress 6.5+ PHP 7.4+

Installing, and what activation actually does

Activation writes one option, mcc_settings, filled with the shipped defaults. It also creates mcc_consent_log and writes a single "activated" entry into it. It uses add_option, so re-activating an installation that already has settings never overwrites them. Nothing reaches visitors yet: with no tracking ID configured, the banner, the Consent Mode bootstrap and the front-end CSS and JavaScript are all withheld. The plugin registers no cron event, no database table, no REST route, and it never touches mail.

  1. Install the ZIP through Plugins → Add New → Upload Plugin, or copy the m-cookie-consent folder into /wp-content/plugins/.
  2. Activate it. The plugin header declares WordPress 6.5 and PHP 7.4 as the minimums.
  3. Open M Consent in the admin sidebar. A Settings shortcut also appears next to the plugin on the Plugins screen. Both labels come from the plugin itself, so they stay in English until someone translates it.
  • Consent mode: "Stay fully blocked", the safest option, is the shipped default.
  • Cookie lifetime: 180 days.
  • Floating settings button: on.
  • Design: Modern, laid out as a floating card, bottom left.
  • Accent colour #6d5cf5, text colour #14171f.
  • Custom script category: Marketing.
  • Consent version: 1.
  • A suggested paragraph is added under Settings → Privacy, in the Policy Guide section.
  • No front-end output at all until at least one tracking integration is filled in.

Where the screens live

The plugin adds one top-level menu, M Consent, with the privacy dashicon, at menu position 80, so it lands low in the sidebar near Settings. Its only subitem is labelled Settings and opens the same page, wp-admin/admin.php?page=m-cookie-consent. The menu, the page render and the re-consent action all require the manage_options capability, so on a normal single site only administrators reach it. The screen is split in two: four setting sections on the left, two panels on the right, Live preview and Status & tools.

  • Tracking integrations: the GA4, Tag Manager, Meta Pixel and custom script fields.
  • Consent behaviour: consent mode, cookie lifetime, floating button, policy links.
  • Banner texts: seventeen optional overrides for the built-in wording.
  • Appearance: design, layout, accent colour, text colour.
  • Live preview: a miniature banner that updates as you type.
  • Status & tools: the list of active services, the current consent version, "Preview on my site" and "Ask all visitors for consent again". It reports what is stored, not what is valid, so an unusable ID still shows up here as an active service.

Tracking integrations, and what each one gates

Every field here ships empty. Fill in only the services you actually use: the banner offers exactly the categories your configuration puts into play, so a site with only GA4 gets a two-category banner and nothing more. Each ID is validated on save and re-validated again at output time before it is printed.

  • Google Analytics 4 ID. Empty by default. Must match G- followed by 4 to 20 letters or digits; stored uppercase. Filling it puts Analytics on the banner, and the tag runs only once Analytics is granted.
  • Google Tag Manager ID. Empty by default. Must match GTM- followed by 4 to 10 letters or digits; stored uppercase. Filling it puts both Analytics and Marketing on the banner, because the container contents are unknown. The container tag itself carries both categories and the script loads it as soon as either one is granted, so a visitor who accepts Analytics alone still pulls in the whole container; the marketing tags inside it are then held back by Consent Mode signals only, not by this plugin. If GA4 already sits inside the container, leave the GA4 field empty.
  • Meta Pixel ID. Empty by default. Digits only, 5 to 20 of them. Runs only once Marketing is granted. There is no noscript iframe fallback, deliberately: a visitor without JavaScript can never see the banner and so can never consent.
  • Custom tracking script. Empty by default. Raw HTML for the head section, script tags included. Only accounts holding the unfiltered_html capability can change it; for everyone else the textarea is read-only and a submitted change is discarded with a notice and the previous script is re-stored. On multisite that capability belongs to super admins only. Read section 8 before you paste anything here: the gating is narrower than the field label suggests.
  • Custom script category. Marketing by default. The choices are Functional, Analytics and Marketing. Necessary is deliberately not offered, so tracking code can never bypass consent.
Careful

An invalid tracking ID is not merely rejected, it is discarded: the field is emptied and an error appears at the top of the screen. Paste a legacy UA- property ID, an ID with a stray space, or a Pixel ID with dashes, and the integration silently ends up switched off, and with it the category it would have added to the banner. Nothing on the screen repeats that afterwards, so after every save read the notices and confirm that Status & tools still lists the service you expect.

Appearance

Four settings control how the banner looks. The two colour pickers feed CSS custom properties that are written both into a :root block and directly onto each banner element, so a CSS-optimisation plugin cannot strip them. Two lighter shades for body text and links are derived from your text colour automatically.

  • Design: Modern is the default, the built-in rounded card with an accent gradient and a cookie icon. "Match my website theme" gives a neutral banner that inherits your theme's font and stays visually unobtrusive.
  • Banner layout: Floating card, bottom left is the default. The alternatives are Bottom bar (full width) and Floating card, bottom right.
  • Accent colour: #6d5cf5 by default. It drives the primary button, the icon, the toggle switches and the floating button.
  • Text colour: #14171f by default. It drives the heading and body text of both the banner and the preferences window.
Careful

Only the primary button label is nailed to white, by #mcc-banner .mcc-btn--primary { color: #ffffff !important }. The banner background is white too, so a light accent colour makes "Accept all" unreadable. The floating button fails differently: its white icon comes from .mcc-reopen { color: #ffffff } with no !important, and in the "Match my website theme" design that rule is overridden to the accent colour on a white circle, so a light accent erases the icon completely. A light text colour is the worst case, because the heading, the banner message, the category titles and descriptions and the "Only necessary" label are all forced from your text colour with !important: the whole banner and the whole preferences window go unreadable at once. Since 1.0.1 these colours are also written straight onto the elements, so no theme, cache or CSS-optimisation plugin will rescue a bad choice. Check the Live preview, then check the real thing with "Preview on my site". The way back is in section 9.

A realistic first run, in order

Do it in this order and you will not have to redo anything. Every change to a tracking ID, the custom script, its category or the consent mode raises the consent version, so batching your changes into one save costs your visitors one prompt rather than five.

  1. Enter only the tracking IDs you actually use. If GA4 already sits inside your Tag Manager container, enter the GTM ID alone and leave GA4 empty.
  2. If you paste a custom script, paste script tags and nothing else, and set its category. Marketing is the default and the safe assumption for anything advertising-related. Section 8 explains what happens to everything that is not a script tag.
  3. Leave "Before consent, Google tags…" on "Stay fully blocked (recommended)".
  4. Set the Privacy policy link, or leave it empty and configure the page under Settings → Privacy. Add a Cookie policy link only if you keep a separate page.
  5. Leave the floating settings button on.
  6. Set the accent and text colours and confirm them in the Live preview.
  7. Leave every Banner texts field empty unless a lawyer told you otherwise.
  8. Save, then read the notices at the top of the screen. A rejected ID is reported there and nowhere else.
  9. Confirm that Status & tools lists exactly the services you expect, and note the consent version.
  10. Purge your page cache and your CDN. Do this before you test, not after: an old cached page carries the old consent version and will make your test lie to you.
  11. Open "Preview on my site", then test for real in a private window: accept, reload, withdraw via the floating button, reject, reload. Withdrawal is not instant on the open page: the denied signal, the fbq revoke and the vendor cookie cleanup all run immediately, but the GA4, GTM and Pixel scripts already injected into that page view keep running until you reload. Judge the result after the reload.
  12. Put [m_cookie_settings] on your privacy or cookie policy page so consent can be withdrawn from a stable URL.

Dangerous operations and how to recover

Nothing in this plugin can lock you out of wp-admin, block a login, touch wp_mail or schedule a cron job. Its front-end class is constructed only when the request is not an admin request, and there is no login, mail, cron or REST integration anywhere in the code. What it can do is break your public pages, load third-party resources before anyone consented, make the banner unusable, invalidate every stored consent, overwrite your saved configuration and destroy your accountability log. Each item below names the exact way back. Read the first four before you ever paste anything into Custom tracking script.

  • Custom tracking script: raw code in the head of every public page. It is never validated. A JavaScript syntax error inside it does not break anything, because the blob is printed as type="text/plain", is never parsed before consent, and after consent is injected as its own script element, so a throw inside it cannot reach banner.js or your theme. Malformed HTML is the real hazard: an unclosed tag or an unclosed comment swallows the rest of the head and takes the page with it. Recovery: open M Consent, clear the field, save and purge your cache. If your account cannot edit the field, that will not work, see the fourth item in this list.
  • The gating only covers scripts, images and iframes. transform_gated_html() neutralises exactly two things: script tags whose type is empty, text/javascript, application/javascript or module, and the src attribute of img and iframe. Everything else is printed exactly as you typed it: link, style with an external url(), object, embed, video, source, noscript, and meta http-equiv="refresh". A stylesheet from a third-party font host therefore loads on first paint for every visitor, before any consent, and defeats the whole point of the plugin. A meta refresh pasted there redirects every front-end page of the site while wp-admin stays reachable. Recovery: paste script tags only; to undo, clear the field as above, or use the WP-CLI command in section 9 if the front end is already redirecting.
  • A script with an unusual type attribute is not gated at all. The rewriter only acts when the type is empty, text/javascript, application/javascript or module. Anything else is returned untouched and carries no gating marker, so the plugin never activates it after consent either. If the browser still treats that type as JavaScript, the script runs on first paint with no consent; if the browser does not, it never runs at all. Recovery: remove the type attribute from your snippet, or move the tracker into a proper integration field.
  • You may not be able to clear the custom script from the settings screen at all. render_custom_script() marks the textarea readonly for anyone without the unfiltered_html capability, and sanitize_custom_script() discards the submitted empty value and re-stores the previous script with a notice. That is every multisite site administrator who is not a super admin, and every install that defines DISALLOW_UNFILTERED_HTML, which many managed hosts do by default. A broken snippet then stays in the head of every public page with no route out through the interface. Recovery, in order of preference: run wp option patch update mcc_settings custom_script '' (the sanitizer is not loaded under WP-CLI); or have a super admin clear it; or remove the DISALLOW_UNFILTERED_HTML constant from wp-config.php and clear it yourself; or deactivate the plugin, which silences the output and keeps every setting.
  • Advanced Consent Mode sends data to Google before any consent exists. Choosing "Load with all consent signals denied" makes every visitor's browser fetch Google's tags and send denied-state pings on first paint. Several EU supervisory authorities treat those pings as processing. Recovery: switch back to "Stay fully blocked (recommended)" and save. The switch is consent-relevant, so the consent version is raised and every visitor is asked again; purge the page cache.
  • No tracking ID configured means no banner at all. The banner, the Consent Mode bootstrap and the front-end assets are printed only when at least one optional category is in play. A site that loads GA4 from its theme, or a Meta Pixel through another plugin, will show no banner and will track everyone. Recovery: move those trackers into this plugin. The mcc_force_banner filter is not an equivalent: it only flips banner_enabled(), so the banner appears while nothing whatsoever is gated, because this plugin blocks only the tags it prints itself. A theme-embedded gtag GA4 is at least held to the denied Consent Mode defaults the bootstrap sets at wp_head priority 0; a Meta Pixel loaded by another plugin fires its PageView before any decision and is not covered at all, because the fbq revoke runs only after the visitor clicks something. Use the filter to collect and broadcast a decision that your own code then honours through the mcc:consent event, never as a substitute for moving the trackers in.
  • Turning off the floating settings button removes the only built-in way to withdraw consent, and the usual fallback may not work. GDPR requires withdrawal to be as easy as consent. The catch is that [m_cookie_settings], #mcc-settings and window.mccShowSettings() all depend on banner.js and on the preferences window, and neither is output unless at least one optional category is in play. On a site whose trackers were later removed, the shortcode renders a button that does nothing. Recovery: switch the floating button back on; and before you ever turn it off, confirm that at least one integration exists or that mcc_force_banner is filtered on.
  • "Ask all visitors for consent again" cannot be undone from the settings screen. It increments consent_version, which instantly invalidates every consent cookie in the wild. The sanitizer will not let that number go down: it takes the higher of the stored and the submitted value. The same bump happens automatically on any change to the GA4 ID, the GTM ID, the Pixel ID, the custom script, its category or the consent mode. Expect a measurement gap while visitors re-consent. Recovery: write a lower consent_version straight into the mcc_settings option, as shown in the next section.
  • Any save on the settings screen writes filtered settings permanently into the database, and so does "Ask all visitors for consent again". handle_force_reconsent() reads the settings through MCC_Plugin::get_settings(), which has already passed through the public mcc_settings filter, and hands that array to update_option(). The ordinary Save Changes path reaches the same place by a different route: every render callback prefills its field from that same filtered array, so the form posts the injected values straight back. Whatever a theme, a snippet or a multilingual plugin injects through that filter is validated by the sanitizer and then stored for good, silently replacing your saved configuration; a translated banner heading injected at runtime becomes your permanent banner heading the first time any administrator saves anything at all. If the injected value touches a consent-relevant key, the version is bumped too, and on the re-consent button that bump lands on top of the button's own. Recovery: remove the filter, then re-enter the correct values on the settings screen, or restore mcc_settings from a database backup.
  • A consent-version bump plus a stale page cache costs your visitors two prompts, not one. The version is baked into the cached HTML by the consent bootstrap, so visitors served stale pages write cookies carrying the old version. The moment the cache is finally purged, the strict version check invalidates every one of those cookies and the same people are asked all over again. Recovery: none after the fact, so purge the page cache and the CDN immediately after any save that raises the version, not the next morning.
  • The accountability log is destroyed by ordinary use, not only by Delete. append_log() keeps the last 50 entries and drops the rest on every single write. A busy week of configuration changes silently overwrites your earliest records, and there is no export button anywhere in the interface. Recovery: none. If you rely on mcc_consent_log as GDPR evidence, copy it out periodically with wp option get mcc_consent_log --format=json, not just before uninstalling.
  • Deleting the plugin destroys both options. Deactivation removes nothing, but Delete on the Plugins screen runs uninstall.php, which deletes mcc_settings and mcc_consent_log. On multisite it iterates every site in the network and deletes both from all of them. Recovery: a database backup, and nothing else.
  • A light text or accent colour makes the banner unreadable for every visitor. The settings screen is unaffected, so this is recoverable, but the Live preview is the only place you will notice before your visitors do. Recovery: set the colours back on the Appearance section, or write the shipped defaults straight into the option as shown in the next section.
Careful

Three of these cannot be undone from the settings screen: raising the consent version, clearing a custom script your account is not allowed to edit, and deleting the plugin. Two destroy data outright: Delete, which on multisite wipes both options from every site in the network, and the log's own 50-entry cap, which quietly discards your oldest records on every write. Take a database backup before you press Delete or "Ask all visitors for consent again". Everything else on this list is reversible, most of it from the settings screen and the rest from the command line. If you are only troubleshooting, deactivate the plugin or rename its folder; both leave every option intact.

Recovery without the settings screen

All of the plugin's state lives in two options and one folder, so a broken front end is always recoverable from the command line. Note one thing before you type: the settings class is instantiated only on admin requests, so its sanitizer never registers under WP-CLI. A write through wp option goes in exactly as you type it, with no validation, no clamping, no capability check on the custom script and no monotonic version guard. That is what makes it a usable escape hatch. It is also why a wrong value here is worse than a wrong value on the screen: write an ID the plugin cannot use, a legacy UA- property or an ID with a space in it, and the category counter still treats the service as in play, Status & tools still lists it, and the output stage silently prints nothing. The result is a banner asking visitors to consent to a tracker that does not exist, with no error on any screen. Read the option back after every patch.

  1. Stop the bleeding first: deactivate the plugin, or rename its folder if even WP-CLI is unavailable. Both silence all output immediately and neither deletes anything.
  2. If only the custom script is at fault, clear that one key and leave the rest of your configuration alone. This is also the first route when your account cannot edit the field on the settings screen; section 8 lists the other three.
  3. If Advanced Consent Mode was the mistake, set consent_mode_style back to basic.
  4. If a colour choice made the banner unreadable, write the shipped values back into text_color and accent_color.
  5. If you raised the consent version by accident, write the previous number back. Visitors whose cookie still carries that version are honoured again.
  6. Read the two options back after every single patch and confirm the change landed as you intended. Nothing validates these writes and nothing reports a bad one.
  7. Purge the page cache and the CDN, then reload the public site in a private window.
  8. Reactivate the plugin, or rename the folder back. Your saved settings are still there.
# Stop all plugin output, keep every setting
wp plugin deactivate m-cookie-consent

# No WP-CLI? Renaming the folder deactivates it just as well
mv wp-content/plugins/m-cookie-consent wp-content/plugins/m-cookie-consent.off

# Clear only the custom tracking script.
# This works even when the settings screen refuses: the readonly textarea and
# the unfiltered_html check live in the admin sanitizer, which is not loaded here.
wp option patch update mcc_settings custom_script ''

# Return to full prior blocking
wp option patch update mcc_settings consent_mode_style basic

# Put the shipped colours back when the banner became unreadable
wp option patch update mcc_settings text_color '#14171f'
wp option patch update mcc_settings accent_color '#6d5cf5'

# Put the consent version back down (bypasses the monotonic guard)
wp option patch update mcc_settings consent_version 3

# Read back what is stored, after EVERY patch above.
# Nothing here is validated, so this is the only place a mistake shows up.
wp option get mcc_settings --format=json
wp option get mcc_consent_log --format=json

Troubleshooting

Check Status & tools first: it gives you the list of active services and the current consent version, which between them explain most surprises. Remember that it reports what is stored rather than what is usable, so a service listed there is not proof that anything is being printed.

  • No banner on the front end, and Status & tools says "Active services: none". Either an ID was rejected on save, or none was ever entered. Re-enter it and watch the notice at the top of the screen.
  • Status & tools lists a service but the page source contains nothing for it. The stored ID does not pass validation, which happens when it was written through WP-CLI or injected by a filter. The banner still asks for that category. Fix the ID on the settings screen.
  • The banner shows but GA4 never fires. Confirm consent was actually given for Analytics, then look in the page source for the tag with id mcc-ga4-loader. In fully blocked mode it ships as type="text/plain" and the script swaps it for a real script tag after consent.
  • The whole GTM container loaded even though the visitor only accepted Analytics. That is by design: the container tag carries both analytics and marketing, and the script activates an element as soon as any one of its listed categories is granted. Only Consent Mode signals hold back the marketing tags inside the container.
  • Trackers keep running right after the visitor withdrew consent. Also by design, and only until the next page load: the denied signal, the fbq revoke and the vendor cookie cleanup run at once, but scripts already injected into the open page are not removed. Test after a reload.
  • Old texts or old colours after an update. Both assets are versioned with MCC_VERSION, so a version bump does refresh them, but a page cache or CDN in front of the site will still serve the old HTML. Purge both.
  • Everyone is being asked again for no apparent reason. Any change to a tracking ID, the custom script, its category or the consent mode raises the consent version, and so does the re-consent button. Compare the Consent version number in Status & tools against what you expect.
  • Everyone is asked twice. The first prompt came from a stale cached page carrying the old version, the second from the purge that invalidated the cookie they had just written. Purge immediately after saving to avoid it next time.
  • The floating button overlaps a chat widget. It is fixed at the bottom left with z-index 99999. Either turn it off and use [m_cookie_settings] or a link to #mcc-settings instead, or restyle .mcc-reopen from your theme.
  • The [m_cookie_settings] button renders but does nothing. banner.js and the preferences window are only output when at least one optional category is in play, so on a site with no integrations left the shortcode is inert. Restore an integration, or turn on the mcc_force_banner filter.
  • Your custom script never runs even after consent. Check its type attribute: anything outside empty, text/javascript, application/javascript and module is skipped by the gating rewriter entirely and is therefore never activated.

Privacy and data

The plugin makes no outbound request of its own. There is no licence check, no usage statistic, no update ping and no GeoIP lookup anywhere in it, and every asset it ships is served from your own domain. The only third-party requests on your site are the ones you configured, and those fire only after consent, with the exception described in section 8: anything other than a script, an image or an iframe pasted into the custom script field is not gated at all.

  • One cookie is set on the visitor's device, mcc_consent. It holds a JSON object with the consent version, a timestamp in milliseconds and the chosen categories. No identifier of any kind.
  • Its lifetime is your "Remember the choice for" value in days. It is scoped to the path of your home URL, SameSite=Lax, and Secure whenever the page is served over HTTPS.
  • Two options are stored in your database. mcc_settings holds your configuration. mcc_consent_log holds the last 50 configuration events, each with a UTC timestamp, the consent version and the list of active services at that moment. Neither contains anything about an individual visitor. The 50-entry cap applies on every write, so older events are gone for good.
  • When Analytics consent is withdrawn, the script expires cookies matching _ga, _ga_ prefixed names, _gid and _gat. Withdrawing Marketing does the same for _fbp and _fbc.
  • That cleanup walks the host and every parent domain, so a visitor who clicks "Only necessary" on shop.example.com also destroys the _ga and _ga_ cookies of www.example.com. This is legally harmless but operationally surprising if the parent site belongs to someone else. Plan for it before you deploy the banner on a subdomain of a shared apex.
  • Settings → Privacy, in the Policy Guide section, receives a suggested paragraph describing all of this, ready to copy into your own policy.

Uninstalling

Deactivating changes nothing but the output: the banner disappears, the tracking snippets stop being printed, and every option stays in the database, so reactivating restores your exact configuration. Deleting the plugin from the Plugins screen is a different act entirely, because it runs uninstall.php.

  • Removed on delete: the mcc_settings option, holding your entire configuration.
  • Removed on delete: the mcc_consent_log option, holding your accountability record.
  • On multisite, uninstall.php iterates every site in the network and deletes both options from all of them, not only from the site you were looking at.
  • Survives: the mcc_consent cookie already on visitors' devices. The server cannot delete a cookie it did not just set; it simply expires on its own schedule.
  • Survives: any paragraph you copied from the Policy Guide into your own privacy policy.
  • Survives: a leftover [m_cookie_settings] in your content, which will then render as literal text, and any theme code hooked to the plugin's filters.
Careful

There is no export button for mcc_consent_log anywhere in the interface, and Delete is not the only thing that destroys it: the log keeps only the last 50 entries and drops the rest on every write, so a busy month erases its own beginning. If you rely on it as your GDPR accountability record, copy it out on a schedule with wp option get mcc_consent_log --format=json, and once more before you press Usuń. Once uninstall.php has run, only a database backup brings it back.

Automatic updates

The plugin checks majevski.com for new releases and offers them through the normal WordPress update screen — the same prompt, changelog popup and one-click install as any other plugin. Checks are cached, never slow a page down, and fail open: if majevski.com cannot be reached, the site simply carries on and tries again later. An update package is only ever accepted from majevski.com over HTTPS, and an older version is never offered. Versions before 1.1.0 predate the update channel, so they cannot see new releases — install 1.1.0 or newer manually once, and every later release arrives on its own.

Want something like this built?

Everything on this page was designed, built and is run by one person. If you need the same for your business, tell me what you have in mind.

Book a call opens in a new tab