Skip to content

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.


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.


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.lastURL lets us override the referrer when the previous navigation was not a swup transition (for example, an internal search that changed the URL via replaceState).
  • After being consumed it is reset to null so the next transition falls back to swup’s data.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.

src/scripts/utilities/setPageViewOnSearch.js
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:

  1. User lands on /blog. swup fires page:viewVirtualPageview with referrer = previous page.
  2. User types in the search box → syncURLParams rewrites the URL to /blog?search=foo with replaceState.
    • setPageViewOnSearch sends VirtualPageview with referrer = /blog.
    • Stores window.lastURL = /blog?search=foo.
  3. User clicks a post → swup fires page:view.
    • Core.js sees window.lastURL and uses it as the referrer (/blog?search=foo) instead of swup’s data.from.url (which would be /blog).
    • Resets window.lastURL = null.

Without that coordination, the referrer of the post would skip the “search applied” state.


  • Project uses swup transitions? → hook page:view as in Core.js.
  • Project has listings with search / pagination via replaceState? → call setPageViewOnSearch after every replaceState.
  • 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 VirtualPageview and map the dataLayer variables to the GA4 tag.
  • Validate with the GTM Preview or Tag Assistant extension that each internal navigation emits a single VirtualPageview with the correct URL and referrer.

Knowledge Check

Test your understanding of this section

Loading questions...