GA4, Google Tag Manager and Cookie Banners on a WordPress SPA: Fixing Double-Fires and Zero Engagement Time
EDITOR NOTE — remove before publishing. The iubenda double-fire and GA4 engagement-time issues are open bugs as of 2.4.2. Update this post when fixes ship.
Analytics tools were built around a simple assumption: one page load, one pageview. A single page application breaks that assumption, and the symptoms are confusing because nothing errors — the numbers just go strange.
Here’s what changes, what to configure, and which problems are ours rather than yours.
The one event that matters
AjaxPress fires ajaxpress:ready after every navigation, once, with the new URL and title:
document.addEventListener('ajaxpress:ready', (e) => {
console.log(e.detail.url, e.detail.title);
});
Almost everything below is a variation on “hook the right thing to this event, and don’t hook the wrong thing to it.”
Google Analytics 4
GA4’s Enhanced Measurement includes a “page changes based on browser history events” setting. It’s on by default, and on most SPA setups it works — GA4 sees the History API change and records a pageview.
The catch: it often records the pageview before the new title is set, so your reports fill up with the previous page’s title against the new URL. That’s why a lot of people turn the setting off — and then get no pageviews at all.
If you turn it off, send them yourself:
document.addEventListener('ajaxpress:ready', (e) => {
gtag('event', 'page_view', {
page_location: e.detail.url,
page_title: e.detail.title
});
});
Because the event fires after the title has been synced, this gets the right title against the right URL.
Don’t do both. Leaving Enhanced Measurement on and sending manual pageviews doubles every number in your reports. Pick one.
Known issue: engagement time dropping to zero
We’ll be straight about this one: “Average engagement time” reading zero under AjaxPress is a known open issue. It was reported by a user, we’ve reproduced the report, and it isn’t fixed yet.
The likely cause is that GA4 measures engagement using timers tied to page visibility and focus, and those don’t restart cleanly for a virtual pageview. Other metrics — pageviews, events, conversions — are unaffected.
If engagement time is a metric you report on, that’s worth knowing before you commit. If you have a working configuration, we’d genuinely like to hear it.
Google Tag Manager
GTM’s default “All Pages” trigger fires on real page loads only. Push your own event instead:
document.addEventListener('ajaxpress:ready', (e) => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'ajaxpress_pageview',
pagePath: e.detail.url,
pageTitle: e.detail.title
});
});
Then in GTM, create a Custom Event trigger on ajaxpress_pageview and attach whichever tags need to fire per page.
Be selective. Not every tag should fire on every virtual pageview. A conversion tag that fires on each navigation will wreck your data.
Cookie and consent banners
Consent scripts assume they initialise once per page load. Under a SPA they can initialise more than once, and the banner appears again — sometimes twice on the same screen.
Known issue: the iubenda consent popup has been reported firing twice with AjaxPress enabled, and once with it disabled. That’s an open bug on our side, reported by someone evaluating the plugin before purchase. It’s being worked on, and the fix targets the general double-initialisation case rather than iubenda specifically.
If you’re hitting it with another consent tool, the pattern to look for is a script that runs on both the initial load and every navigation. Guard it:
if (!window.__consentInitialised) {
window.__consentInitialised = true;
// initialise the consent tool here
}
Consent state is one of the few things that genuinely should not re-initialise per navigation — someone who accepted on the home page shouldn’t be asked again three pages later.
Sliders, lightboxes, counters and other widgets
The opposite problem: these do need re-initialising, because the elements they were bound to have been replaced.
document.addEventListener('ajaxpress:ready', () => {
if (window.jQuery) {
jQuery('.your-slider').slick(); // or whichever library
jQuery('.gallery a').magnificPopup();
}
});
If a library complains about being initialised twice, destroy before re-creating:
document.addEventListener('ajaxpress:ready', () => {
const el = jQuery('.your-slider');
if (el.hasClass('slick-initialized')) el.slick('unslick');
el.slick();
});
A quick way to tell which problem you have
| Symptom | Likely cause |
|---|---|
| Double the pageviews | Enhanced Measurement on and manual pageviews |
| No pageviews after the first | Enhanced Measurement off, nothing sending manually |
| Right URL, wrong page title | Pageview firing before the title syncs — send manually on ajaxpress:ready |
| Consent banner appears twice | Double initialisation — guard it, or the known iubenda issue |
| Slider works on first load only | Needs re-initialising on ajaxpress:ready |
| Engagement time zero | Known open issue on our side |
Testing it properly
Open GA4’s DebugView, or GTM Preview mode, then navigate through five or six pages on your site and watch what arrives. You want exactly one pageview per navigation, with matching URL and title.
Test the browser Back button too. It’s a navigation like any other, and it’s where duplicate-firing bugs usually reveal themselves.
Getting help
Email [email protected] with your analytics setup (GA4 direct or via GTM), whether Enhanced Measurement is on, any custom code you’ve added, and a URL. Analytics issues are hard to diagnose blind and easy to diagnose with a live page.