Consent banner
Beaconry ships its own two-button consent banner so you do not need a separate cookie plugin to be compliant. It follows the strict EU model: nothing is tracked until the visitor actively accepts. No pre-ticked boxes, no "by continuing you agree", no events on the wire before a click. The banner is the nl-data-gate module, the same engine the rest of the pipeline reads its consent state from.
Why a built-in banner
Server-side tracking does not exempt you from consent. Whether the request goes browser-to-Meta or server-to-Meta, you are still processing a visitor's data for advertising, and in the EU that needs a lawful basis. Beaconry's whole pipeline is gated on one signal, the nl_pref cookie, and the banner is what writes it. You can bring your own consent manager instead (see "Turning the banner off" below), but if you do not have one, the bundled banner makes Beaconry compliant out of the box with zero extra plugins.
One thing to be clear about: the gate is real. It stops the entire pipeline, server-side dispatch included, not just browser pixels. Until nl_pref carries analytics: true, the browser engine fires nothing and the REST endpoint rejects what little might reach it. No accept means no tracking, full stop.
Two-button, no implicit consent
The banner renders exactly two actions plus a privacy link:
- Accept writes consent with analytics and marketing set to
trueand lets the pipeline run. - Reject writes consent with analytics and marketing set to
false. The decision is stored, so the visitor is not nagged on every page, but nothing is tracked. - Privacy link points at your privacy policy (configurable, see below).
There is no implicit-consent path on purpose. Scrolling does not accept. Navigating to the next page does not accept. Closing the banner with Escape counts as reject, not accept, because dismissing a dialog must never be read as agreement. The only way to start tracking is an explicit click on Accept. That is the behavior EU "no implicit consent" rules require, and it is the default with no configuration.
Until a decision exists, the banner simply stays up and no consent cookie is written. On the first page where the visitor clicks Accept, the cookie is set and a nl:gate:changed event fires so the tracking engine can start immediately on that same page, no reload needed.
The nl_pref cookie
The single source of truth for consent is a first-party cookie named nl_pref, written client-side with path=/; SameSite=Lax and (on HTTPS) the Secure flag. It holds a JSON object, not a bare flag:
{"analytics":true,"marketing":true,"functional":true,"timestamp":1733650800,"version":1}What each field means:
analyticsis the gate Beaconry actually reads.truelets events flow,falseblocks them.marketingtracks the same accept/reject decision (the banner is binary, so the two move together) and is used when withdrawing consent to know which tracker cookies to purge.functionalis alwaystrue; strictly necessary cookies do not need opt-in.timestampis the Unix second the decision was made, useful for proof-of-consent records.versionlets the schema evolve without misreading old cookies.
The cookie lasts one year (max-age 365 days), then the banner reappears so consent is periodically renewed rather than assumed forever. The value is read back defensively: anything that is not valid JSON with a boolean analytics key is treated as "no decision yet" and the banner shows again, so a corrupted or hand-edited cookie can never silently enable tracking.
Withdrawing consent actively cleans up. Per GDPR Article 7(3), revoking has to be as easy as granting. When a visitor flips from accept to reject, the engine does not just rewrite nl_pref, it also expires the common first-party tracker cookies that were set during the consented session (Google Analytics _ga* / _gcl_*, the engine's own nl_sid / nl_dev / nl_ref / nl_ext, LinkedIn li_fat_id, Meta _fbp / _fbc, TikTok _ttp / ttclid and friends) and, if a Meta Pixel is present, calls fbq('consent', 'revoke') so it stops sending immediately. The visitor gets a clean cookie jar at withdrawal, not just a flag flip.
Customizing on the Banner tab
Everything visitor-facing lives under wp-admin, Beaconry, Cookie Banner. Nothing here is required, every field falls back to a sensible bundled default, so a fresh install already has a working, localised banner.
Enable / disable
A single master toggle, "Show consent banner on first visit". The status badge next to it reads Visible or Hidden so you can see the current state at a glance.
Copy
Four text fields, each optional, each falling back to the localised default when left blank:
| Field | What it sets | Default (EN) |
|---|---|---|
| Message | The banner body text | We use cookies for anonymous reach measurement. You can revoke at any time. |
| Accept button | Label on the accept action | Accept |
| Reject button | Label on the reject action | Reject |
| Privacy link text | Label on the privacy link | Privacy |
The bundled defaults are hardcoded per locale (German and English ship in the engine) because the banner fires very early in the page lifecycle, before the full WordPress translation stack is ready, so it cannot rely on a late-loaded locale override for its first paint.
Colors
Three optional color fields, "Background", "Text color" and "Accent (buttons)". Leave any of them blank and the banner inherits that token from your active theme, Beaconry only overrides a banner color when you explicitly set a value. Each field has a native color picker next to a hex input so you can type #ff6a1a or pick it visually. The accent color paints the Accept button; the reject action stays a quiet underlined text button by design so the primary choice is visually obvious without dark patterns.
Privacy URL
Where the privacy link points. Leave it blank and Beaconry links to /datenschutz/ on the current site. Set it to any URL to point elsewhere. On multilingual installs this single URL is resolved to the right per-language page at request time, covered in the next section.
Re-open the banner later
Consent has to be revocable. Drop the [beaconry_revoke_consent] shortcode on your privacy page (or anywhere) and it renders an inline "manage cookies" link that wipes nl_pref and re-shows the banner so the visitor can change their mind. Override the label with [beaconry_revoke_consent text="Manage cookies"]. Under the hood the link carries the data-nl-cookie-settings attribute that the banner engine listens for, so it re-opens the live dialog rather than just reloading.
Accessibility
The banner is a proper modal dialog, not a styled div, and it behaves like one for keyboard and screen-reader users:
role="dialog"+aria-modal="true"with a localisedaria-label("Cookie settings" / "Cookie-Einstellungen"), so assistive tech announces it as a modal dialog with a name.- Focus moves into the dialog on show. When the banner slides in, focus lands on the Accept button, so a keyboard or screen-reader user is immediately inside the dialog rather than hunting for it.
- Focus is trapped. Tab and Shift+Tab cycle only between the privacy link and the two buttons; you cannot Tab out into the page behind the dialog while a decision is pending.
- Escape dismisses as reject. Per WCAG 2.1, a dialog must respond to Escape; here Escape stores a reject decision (never a silent accept) and closes the banner.
- Visible focus indicators. Buttons and the privacy link have explicit
:focus-visibleoutlines, so keyboard focus is always visible regardless of theme. - After a decision, focus is released. The active element is blurred on accept or reject so focus returns to the document flow cleanly.
It also respects prefers-reduced-motion: the slide-in transition is replaced by a plain opacity fade for visitors who asked the OS to reduce motion. And it is rendered off the critical path (in an idle callback) so it never blocks first paint or hurts your Largest Contentful Paint.
Multi-language (WPML and Polylang)
Two things have to be localised on a multilingual site: the banner text, and the privacy URL (a German visitor should land on /datenschutz/, an English one on /privacy-policy/). Beaconry handles both, and the right place to translate depends on which plugin you run. The Banner tab shows a "WPML detected" or "Polylang detected" hint pointing you at the exact screen.
Banner text
The four copy fields (message, accept, reject, privacy link text) plus the privacy URL are declared as translatable strings:
- WPML. Beaconry ships a
wpml-config.xmlthat declares the banner option keys as admin texts, so they appear automatically in WPML, String Translation under theadmin_texts_bcnr_settingsdomain. Translate each field per active language there; WPML returns the translated value when the settings are read on a non-default-language request, so Beaconry's normal read path picks up the translation transparently with no extra wiring. - Polylang. Polylang has no declarative config file, so Beaconry registers the same keys via
pll_register_stringat admin time. They surface under Languages, Translations in theBeaconrystring group. Only fields you actually filled in are registered (an empty field stays on its bundled localised default and does not clutter the list).
If you leave the text fields blank entirely, you still get correct per-language copy from the bundled German and English defaults, no translation step needed for those two. The String Translation route is for when you want your own wording per language.
Per-language privacy URL
You enter one privacy URL on the Banner tab, and Beaconry resolves it to the visitor's language at request time. The resolver runs through the bcnr_privacy_url filter:
- If no multilingual plugin is active, the URL passes through untouched.
- The configured URL is mapped to a WordPress post via
url_to_postid(). If it does not resolve to a post (a static path, an external URL), it is returned as-is, no guessing. - Otherwise Beaconry asks WPML (
wpml_object_id) or Polylang (pll_get_post) for the translation of that page in the current language and returns the translated permalink. - If there is no translation for the current language, it falls back to the original URL. The resolver never returns empty.
So you point the banner at, say, your English privacy policy once, create its German translation as a normal WPML/Polylang page translation, and German visitors automatically get the German permalink in the banner link. No second URL field, no per-language override list.
For exotic setups (TranslatePress, MultilingualPress, a custom build) the detection is filterable via bcnr_is_multilingual, and you can hook bcnr_privacy_url directly to supply your own resolver.
Turning the banner off (bring your own consent)
If you already run a dedicated consent management platform, turn Beaconry's banner off and let your CMP drive consent instead. The contract is simple: your CMP must write the nl_pref cookie itself. Concretely:
- The cookie name must be exactly
nl_pref. - The value must be a JSON object containing
"analytics"with a boolean (for example{"analytics": true, "marketing": true}). - It must be set before any page load you want to track.
With the banner off and no cookie present, nothing fires, that is the safe default, not a bug. Only turn the banner off if you have an external solution writing the cookie, or if your site genuinely operates in a region with no consent obligation. There is also a power-user nuance for non-EU sites: with the banner disabled and no existing cookie, the loader writes a permissive nl_pref so tracking can run without a banner. Use that only where you are certain consent is not legally required.
Does the marketing site use this?
No, and that is deliberate. beaconry.app ships no GA4, no Meta Pixel, no tracking engine and no cookie banner, because there is nothing to consent to. A product that sells privacy-respecting tracking should not quietly track the people reading about it. The banner described here is what your visitors see on your site once you install the plugin and switch it on.