Wie Beaconry LiteSpeed, WP Rocket und Cloudflare übersteht
Cache- und JS-Optimierungs-Plugins sind die stillen Killer des Conversion-Trackings. Delay-JS wartet auf einen Klick, der oft nie kommt, Combine ordnet deinen Init-Block um, Rocket Loader schreibt dein Script-Tag um. Beaconry nimmt sich selbst aus jedem JS-Optimizer aus, den es gegen die Doku des Herstellers verifizieren kann, und frischt seinen REST-Nonce im Browser auf, sodass ein stundenlang offener Tab oder eine aus dem Full-Page-Cache ausgelieferte Seite weiter gültige Events sendet. Hier ist genau, was es tut, und die zwei Fälle, die du noch von Hand erledigen musst.
Zwei Fehlermodi, und sie sind unterschiedlich
Die Leute werfen "Cache-Plugin hat mein Tracking kaputtgemacht" in einen Topf. Es sind in Wahrheit zwei voneinander unabhängige Probleme, und Beaconry löst sie mit zwei voneinander unabhängigen Mechanismen.
Das erste ist JavaScript-Optimierung: Delay JavaScript Execution, Combine JS, Minify und Cloudflare Rocket Loader. Diese schreiben das Script um, das deine Events auslöst, oder planen es neu ein. Das zweite ist Full-Page-HTML-Caching: dasselbe HTML-Dokument, inklusive des eingebetteten Security-Nonce, wird an Tausende Besucher und an denselben Besucher Stunden später ausgeliefert. Der Nonce darin wird abgestanden. Page-Caching fasst dein JavaScript überhaupt nicht an, also bringt ein Fix für das eine nichts fürs andere.
Wenn du nur an JS-Optimierung denkst, lieferst du ein Plugin aus, das auf jeder Site mit einem langlebigen Tab oder aggressivem Page-Cache trotzdem dunkel wird. Wenn du nur an Page-Caching denkst, lieferst du eines aus, dessen Init-Block ins Nirwana deferred wird. Du brauchst beides.
Warum Delay-JS das ist, das wirklich wehtut
Die meisten JS-Optimizer verschlechtern das Tracking. Delay JavaScript Execution zerstört es. Das Feature ist gebaut, um jedes nicht-kritische Script bis zur ersten Nutzer-Interaktion (Scroll, Mousemove, Touch, Taste) zurückzuhalten, damit die Seite einen tollen Lighthouse-Score mit null JS auf dem kritischen Pfad erreicht. Wunderbar für Paint-Metriken. Tödlich für ein Pageview-Event, weil ein großer Anteil der Sessions, Bounces, One-Tap-und-weg-Mobile-Besuche, vorgeladene Seiten, diese erste Interaktion nie erzeugt. Keine Interaktion, kein Script, kein Event. Die Conversion ist passiert und deine Analytics hat nie davon gehört.
Combine JS und Rocket Loader sind subtiler. Sie blockieren die Ausführung nicht rundweg, sie ordnen sie um. Beaconry enqueued zwei Dinge: die externe Datei nl-data.js (deferred, im Footer) und einen kleinen Inline-Block, der NLData.init() mit der Runtime-Config und dem Security-Nonce der Seite aufruft. Wenn ein Combiner die externe Datei in ein Bundle zusammenführt, aber den Inline-Init dort lässt, wo er war, oder Rocket Loader das Datei-Tag async umschreibt, kann der Inline-Aufruf laufen, bevor die Engine existiert, von der er abhängt. Der Pageview wirft einen Fehler, statt zu feuern.
Das Ziel ist also eng und spezifisch: Beaconrys zwei Scripts aus jedem JS-Optimizer heraushalten, ohne diese Optimizer für den Rest der Site abzuschalten. Der Kunde behält seinen 95+ Lighthouse-Score auf allem anderen. Nur zwei Script-Handles werden ausgenommen.
Das Zwei-Flächen-Problem
Hier ist das Detail, über das die meisten Tracking-Plugins stolpern. WordPress gibt dir den Filter script_loader_tag, um das HTML-Tag eines Scripts umzuschreiben, womit du ein Opt-out-Attribut hinzufügst. Aber dieser Filter sieht immer nur das externe Datei-Tag, das <script src="...nl-data.js">. Er sieht nie den separaten Inline-Block, den wp_add_inline_script() ausgibt, den, der tatsächlich NLData.init() aufruft und den Pageview feuert. Dieser Inline-Block rendert als eigenes <script id="bcnr-nl-data-js-after">-Element, und script_loader_tag kann es nicht anfassen.
Das heißt, ein attribut-basiertes Opt-out schützt die Engine-Datei, lässt aber die Zeile ungeschützt, die sie ausführt. Du musst beide Flächen schützen, und sie brauchen unterschiedliche Mechanismen:
- Die externe Datei wird mit Tag-Attributen geschützt (für Cloudflare und WP Rocket Delay) plus handle-basierten Exclude-Filtern (für LiteSpeed und SiteGround).
- Der Inline-Init-Block wird mit Content-Substring-Exclude-Filtern geschützt, weil es kein Tag-Attribut zum Hinzufügen gibt. Beaconry registriert den stabilen Substring
NLData.initauf den zwei Optimizern, die einen dokumentierten Inline-Exclude-Hook bereitstellen.
Die zwei fraglichen Handles sind bcnr-nl-data und bcnr-nl-data-gate (die Tracking-Engine und die Consent-Banner-Engine). Alles weiter unten operiert auf genau diesen beiden und nichts sonst, also ist es idempotent und nebenwirkungsfrei für den Rest deiner Scripts.
Die doku-verifizierte Opt-out-Matrix
Das ist der Teil, in dem die GOTT-REGEL ihren Wert beweist: jeder einzelne Marker unten wurde wortwörtlich gegen die eigene Dokumentation des Cache-Plugins bestätigt, nicht gegen einen Blog-Post, nicht gegen einen Forum-Thread. Ein adversarialer Verifizierungs-Durchgang hat 30 von 41 Kandidaten-Markern gekillt, weil sie Folklore waren, die in keiner offiziellen Doku auftaucht. Hier ist, was es geschafft hat.
Externes Script-Tag
Beaconrys script_loader_tag-Filter stellt seinen eigenen zwei Tags zwei Attribute voran, in der Reihenfolge, die die Hersteller verlangen:
<script data-cfasync="false" nowprocket
src="...assets/vendor/nl-data/nl-data.js" defer></script>data-cfasync="false"sagt Cloudflare Rocket Loader, das Script zu ignorieren. Cloudflare verlangt, dass dieses Attribut vorsrcerscheint, weshalb Beaconry es direkt nach dem öffnenden<scriptinjiziert.nowprocketsagt WP Rocket, niemals Delay JavaScript Execution auf die Datei anzuwenden. Das ist der Marker, der am meisten zählt, weil Delay-JS der Optimizer ist, der das Tracking voll bricht, statt es nur umzuordnen.
Handle-basierte Excludes (LiteSpeed und SiteGround)
LiteSpeed Cache und SiteGround Speed Optimizer stellen kein Tag-Attribut bereit. Sie stellen Filter-Hooks bereit, die eine Liste von Script-Handles nehmen. Beaconry hängt seine zwei Handles an jeden an:
- LiteSpeed, drei Filter, die jede JS-Achse abdecken:
litespeed_optimize_js_excludes(Combine und Minify),litespeed_optm_js_defer_exc(Defer und Delay),litespeed_optm_gm_js_exc(Guest Mode JS, die optimierte Variante, die LiteSpeed an ausgeloggte Besucher ausliefert). - SiteGround, drei Filter:
sgo_js_async_exclude(async und defer),sgo_js_minify_exclude(minify),sgo_javascript_combine_exclude(combine).
Inline-Block-Excludes (WP Rocket und SiteGround)
Für den Inline-NLData.init()-Block stellen zwei Optimizer einen dokumentierten Content-Substring-Exclude bereit. Beaconry registriert den Substring NLData.init auf beiden:
- WP Rocket:
rocket_defer_inline_exclusions, sodass der Init-Block nie deferred oder delayed wird. - SiteGround:
sgo_javascript_combine_excluded_inline_content, sodass der Init-Block nie in ein kombiniertes Bundle gefaltet wird.
All das wird automatisch beim Plugin-Load registriert. Es gibt keine Einstellung zum Umlegen. Wenn du eines dieser Plugins fährst, sind die Filter bereits angehängt und sie machen sauber nichts, falls das Plugin fehlt.
Die zwei manuellen Ausnahmen
Ehrlichkeit zählt mehr als eine saubere Kompatibilitäts-Tabelle. Zwei Optimizer können nicht automatisch ausgenommen werden, weil der Hersteller keinen Code-Level-Hook für sie ausliefert. Wenn du diese spezifischen Features an hast, fügst du je eine Zeile von Hand hinzu.
- WP Rocket, Combine JavaScript. Es gibt keinen dokumentierten Filter oder kein Attribut, um eine Datei oder ein Inline-Script aus Combine auszuschließen, nur die Admin-UI-Felder unter File Optimization. Wenn du Combine JS aktivierst, füge
nl-data.jszu "Excluded JavaScript Files" hinzu und einNLData.init-Snippet zu "Excluded Inline JavaScript". Wert zu wissen: Combine ist Legacy und standardmäßig aus bei HTTP/2 (was inzwischen fast jeder Host ist). Delay-JS ist das WP-Rocket-Feature, das das Tracking tatsächlich bricht, und das IST automatisch ausgenommen vianowprocket. In der Praxis müssen die meisten WP-Rocket-Nutzer also nichts tun. - Cloudflare, Auto Minify (Legacy) oder APO JS Combine. Das sind Zone-Level-Dashboard-Toggles unter Speed, Optimization, ohne Per-Script-Origin-Marker. Minify allein bricht die Ausführungs-Reihenfolge nicht, also ist es harmlos. Rocket Loader ist der einzige Cloudflare-JS-Optimizer, der die Ausführung umordnet, und der IST automatisch via
data-cfasync="false"abgedeckt. Wenn du APO mit JS Combine fährst, schließe den Pfad aus oder lass JS Combine aus.
Alles andere ist automatisch abgedeckt. WP Super Cache, W3 Total Cache, Autoptimize und WP Fastest Cache im reinen Page-Cache-Modus erreichen Beaconrys JavaScript nie, also brauchen sie überhaupt keinen Exclude. Wenn du eines davon mit aggressivem JS Combine eingeschaltet fährst, gilt derselbe generische Rat: schließe nl-data.js aus der JS-Optimierung aus. Page-Caching allein ist bereits sicher, was genau der Punkt des nächsten Abschnitts ist.
Page-Caching und der abgestandene Nonce
Jetzt der zweite Fehlermodus. Beaconrys REST-Endpoint, /wp-json/beaconry/v1/event, ist durch einen WordPress-Nonce geschützt. Die Inline-Config bettet beim Render einen frischen wp_create_nonce('beaconry_event') ein, nl-data hängt ihn an jeden Event-POST, und der Server verwirft jedes Event, dessen Nonce nicht verifiziert. Dieser Nonce ist Teil der Anti-Abuse-Schicht: ohne ihn könnte jeder im offenen Internet gefakte Conversions POSTen, dein Meta-CAPI-Kontingent verbrennen oder deine GA4-Reports vergiften.
WordPress-Nonces sind zeitgebunden. Sie rotieren etwa alle 12 Stunden und bleiben rund 24 Stunden gültig. Das ist in Ordnung für eine frisch gerenderte Seite. Es wird ein Problem in dem Moment, in dem Full-Page-Caching ins Spiel kommt, denn jetzt ist der Nonce in ein gecachtes HTML-Dokument eingefroren:
- Ein Besucher öffnet einen Tab und lässt ihn 14 Stunden offen. Der zur Ladezeit eingebettete Nonce ist seither herausrotiert.
- Dein Page-Cache liefert ein HTML-Dokument aus, das vor 20 Stunden generiert wurde. Jeder Besucher, der darauf landet, bekommt einen Nonce, der bereits außerhalb seines Fensters ist.
- Eine Gast-Session rotiert unter einer langlebigen Seite weg.
In jedem Fall ist der eingebettete Nonce jetzt einer, den der REST-Endpoint ablehnt, also wird jeder Event-POST stillschweigend verworfen. Der Nutzer ist noch auf deiner Site, konvertiert noch, und die Events verdunsten an der Tür.
Client-seitiger Nonce-Refresh
Der Fix lebt in der Browser-Engine und ist komplett unabhängig davon, welches Cache-Plugin du fährst, das ist es, was ihn robust macht. nl-data stempelt den Timestamp, wann es seinen Nonce erhalten hat. Bevor es ein Event überträgt, prüft es das Alter des Nonce. Wenn die Seite länger offen ist als das Refresh-Fenster, holt es einen frischen Nonce von einem dedizierten GET-Endpoint, tauscht ihn ein und sendet dann:
function ensureFreshNonce( cb ) {
if ( !_config || !_config.nonce ) { cb(); return; }
if ( ( Date.now() - _nonceTs ) < NONCE_REFRESH_MS ) { cb(); return; }
var url = _config.endpoint.replace( /\/event$/, '/nonce' );
fetch( url, {
method: 'GET',
credentials: 'same-origin',
headers: { 'Accept': 'application/json' },
} )
.then( function ( r ) { return r && r.ok ? r.json() : null; } )
.then( function ( j ) {
if ( j && j.nonce ) { _config.nonce = j.nonce; _nonceTs = Date.now(); }
} )
.catch( function () { /* keep existing nonce on failure */ } )
.then( function () { cb(); } );
}Die Details, die das sicher statt clever machen:
- Das Refresh-Fenster ist 6 Stunden, bequem innerhalb des ~24-Stunden-Gültigkeits-Fensters. So kommt ein Nonce nie nahe ans Ablaufen, bevor er ersetzt wird, und der Refresh-Fetch feuert höchstens eine Handvoll Mal über eine ganztägige Session, nicht bei jedem Event.
credentials: 'same-origin'sendet die Session-Cookies mit, sodass der neu erzeugte Nonce an dieselbe Gast-Session gebunden ist, die der Besucher bereits hat. Ein ohne das geholter Nonce würde gegen eine andere Session verifizieren und wäre nutzlos.- Ein Fehlschlag ist nicht fatal. Wenn der Fetch einen Fehler wirft, behält das
.catchden bestehenden Nonce und das Event versucht trotzdem zu senden. Ein abgestandener-aber-vielleicht-gültiger Nonce schlägt kein Event. - Frische Seiten und Worker-Endpoints kurzschließen. Eine Seite, die jünger als 6 Stunden ist, ruft den Callback synchron ohne Netzwerk-Hop auf, sodass normaler Traffic nie verzögert wird. NETZLICHTs Worker-basierte Framework-Sites tragen überhaupt keinen Nonce, also wird der ganze Pfad für sie übersprungen.
Den Nonce-Endpoint selbst uncacheable halten
Es gibt eine letzte Falle. Der Refresh-Endpoint, /wp-json/beaconry/v1/nonce, ist ein GET-Request, und Full-Page-Caches wie LiteSpeed cachen bereitwillig GET-Responses auf /wp-json/*. Wenn diese Response gecacht wird, gibt der Refresh denselben eingefrorenen Nonce aus, den er eigentlich ersetzen sollte, und du bist wieder dort, wo du angefangen hast. Also markiert sich der Endpoint auf beiden Achsen als nicht-cacheable:
do_action( 'litespeed_control_set_nocache', 'beaconry nonce must always be fresh' );
$response = new WP_REST_Response( [ 'nonce' => wp_create_nonce( 'beaconry_event' ) ], 200 );
$response->header( 'Cache-Control', 'no-store, max-age=0' );Der Standard-Header Cache-Control: no-store deckt die meisten Proxies und CDNs ab. LiteSpeed ignoriert reines Cache-Control für seine eigenen Cache-Entscheidungen, also feuert der Endpoint zusätzlich litespeed_control_set_nocache, LiteSpeeds dokumentierte Control-API. Das do_action ist ein harmloser No-op, wenn LiteSpeed nicht installiert ist. Den Nonce auf einem öffentlichen GET freizulegen, fügt keine Angriffsfläche hinzu, derselbe Nonce sitzt bereits in der Inline-Config jeder Seite offen sichtbar, und das echte Gate ist das Per-IP-Rate-Limit plus die Consent- und Nonce-Checks auf dem Event-Endpoint selbst.
Wie du es auf deiner eigenen Site verifizierst
Du musst der Tabelle nicht vertrauen. Bestätige es im Browser:
- Quelltext auf einer gecachten Seite ansehen. Finde das
<script>-Tag fürnl-data.js. Es solltedata-cfasync="false"undnowprockettragen. Wenn diese Attribute vorhanden sind, sind Rocket Loader und WP Rocket Delay beide ausgenommen. - DevTools-Network-Tab, lass den Tab offen. Nachdem die Seite mehr als 6 Stunden offen war (oder senke die Konstante temporär auf einer Staging-Kopie), sollte dem nächsten Event ein GET auf
/wp-json/beaconry/v1/noncevorausgehen, der einen frischen Wert zurückgibt, und der folgende Event-POST trägt ihn. - Prüfe die Response-Header auf diesem GET. Er sollte
Cache-Control: no-storezurückgeben. Triff ihn zweimal und bestätige, dass du zwei verschiedene Nonce-Werte bekommst, Beweis, dass der Cache ihn nicht einfriert. - Beobachte den Event-POST. Ein 204 mit dem Nonce im Request-Body heißt, der Server hat es akzeptiert. Wenn du je siehst, dass Events stoppen, nachdem die Seite offen liegt, ist der Nonce-Pfad der erste Ort, an dem du nachschaust.
Fazit
Server-seitiges Tracking übersteht einen Cache-Stack nicht automatisch. Die Events entstehen weiterhin aus einem Browser-Script, und dieses Script lebt in derselben feindlichen Umgebung wie jedes andere auf der Seite: Optimizer, die es verzögern, Combiner, die es umordnen, Page-Caches, die seine Credentials einfrieren. Beaconry behandelt das als zwei separate Engineering-Probleme. Für die JS-Optimierung nimmt es seine zwei Handles aus jedem Marker aus, den es gegen die eigene Doku eines Herstellers verifizieren kann, und sagt dir klar von den zwei Fällen (WP Rocket Combine, Cloudflare APO Combine), bei denen kein Code-Hook existiert. Fürs Page-Caching frischt es den REST-Nonce im Browser auf einem 6-Stunden-Zyklus auf, an die Session des Besuchers gebunden, weich fehlschlagend, mit dem Refresh-Endpoint selbst uncacheable gehalten. Das Ergebnis ist Tracking, das auf einer gehärteten, stark gecachten WordPress-Site weiter funktioniert, ohne dich zu bitten, den Cache zu schwächen.