VirtualPageview
VirtualPageview is a custom window.dataLayer event sent to Google Tag Manager (GTM) to register page changes that do not trigger a full page reload. It is required when a project uses swup for page transitions, and for any flow (search, pagination) that updates the URL via history.replaceState without reloading.
Without this event, GTM would only register the initial pageview and every subsequent navigation would be invisible to Analytics / Tag Manager.
Event payload
Section titled “Event payload”window.dataLayer.push({ event: "VirtualPageview", virtualPageURL: window.location.href, // full URL virtualPageTitle: document.title, // <title> of the page virtualPagePath: window.location.pathname, // path without hostname virtualPageReferrer: referrer, // previous page});In GTM, configure a Custom Event Trigger listening for the event name VirtualPageview and map the four virtualPage* variables from the dataLayer to the GA4 / pageview tag.
Where it fires
Section titled “Where it fires”There are two active emitters for the same event, plus utility code to coordinate the referrer between them.
1. On every swup page transition — src/scripts/Core.js
Section titled “1. On every swup page transition — src/scripts/Core.js”This is the main trigger. It hooks into swup’s page:view event, which runs after every page change via transition.
// src/scripts/Core.js (≈ lines 184-195)this.swup && this.swup.hooks.on("page:view", async (data) => { if (!window.dataLayer) window.dataLayer = [];
window.dataLayer.push({ event: "VirtualPageview", virtualPageURL: window.location.href, virtualPageTitle: document.title, virtualPagePath: window.location.pathname, virtualPageReferrer: window.lastURL ? window.lastURL : window.location.protocol + "//" + window.location.host + data?.from?.url, }); window.lastURL = null;});Notes:
window.lastURLlets us override the referrer when the previous navigation was not a swup transition (for example, an internal search that changed the URL viareplaceState).- After being consumed it is reset to
nullso the next transition falls back to swup’sdata.from.url.
2. On URL changes without transition — src/scripts/utilities/setPageViewOnSearch.js
Section titled “2. On URL changes without transition — src/scripts/utilities/setPageViewOnSearch.js”When a component updates the URL via history.replaceState (filtering, pagination, search), swup’s page:view does not fire, so the event has to be pushed manually with this utility.
export const setPageViewOnSearch = ({ referrer }) => { if (!window.dataLayer) window.dataLayer = [];
window.dataLayer.push({ event: "VirtualPageview", virtualPageURL: window.location.href, virtualPageTitle: document.title, virtualPagePath: window.location.pathname, virtualPageReferrer: referrer, });
window.lastURL = window.location.href;};The line window.lastURL = window.location.href is important: it makes the next swup transition use this URL as the referrer (instead of the pre-search URL).
3. Utility consumer — src/scripts/handler/searchBlogs/SearchBlogs.js
Section titled “3. Utility consumer — src/scripts/handler/searchBlogs/SearchBlogs.js”Called when the blog search syncs its query params (?search=, ?page=):
// src/scripts/handler/searchBlogs/SearchBlogs.js (≈ lines 92-110)syncURLParams({ search, page } = {}) { const referrer = window.lastURL || window.location.href; // ... mutate url.searchParams ... window.history.replaceState({}, "", url); setPageViewOnSearch({ referrer });}Referrer coordination between both emitters
Section titled “Referrer coordination between both emitters”The non-obvious detail is how the referrer is kept consistent across Core.js (swup) and setPageViewOnSearch.js:
- User lands on
/blog. swup firespage:view→VirtualPageviewwith referrer = previous page. - User types in the search box →
syncURLParamsrewrites the URL to/blog?search=foowithreplaceState.setPageViewOnSearchsendsVirtualPageviewwith referrer =/blog.- Stores
window.lastURL = /blog?search=foo.
- User clicks a post → swup fires
page:view.Core.jsseeswindow.lastURLand uses it as the referrer (/blog?search=foo) instead of swup’sdata.from.url(which would be/blog).- Resets
window.lastURL = null.
Without that coordination, the referrer of the post would skip the “search applied” state.
Implementation checklist
Section titled “Implementation checklist”- Project uses swup transitions? → hook
page:viewas inCore.js. - Project has listings with search / pagination via
replaceState? → callsetPageViewOnSearchafter everyreplaceState. - Project uses Boostify or another loader that delays GTM? → the first pageview is already emitted by GTM on load; only replicate for SPA navigations.
- In GTM: create a Custom Event Trigger named
VirtualPageviewand map the dataLayer variables to the GA4 tag. - Validate with the GTM Preview or Tag Assistant extension that each internal navigation emits a single
VirtualPageviewwith the correct URL and referrer.
Knowledge Check
Test your understanding of this section
Loading questions...