• Courses
    • Paid courses
    • Course bundles
    • Free courses
  • Blog
  • Resources
    • Youtube channel
    • E-books and Guides
    • GTM Recipes
    • View All Resources
    • GTM Community
    • GA4 community
  • Services
  • About
    • About
    • Contact
  • Login
  • Courses
    • Paid courses
    • Course bundles
    • Free courses
  • Blog
  • Resources
    • Youtube channel
    • E-books and Guides
    • GTM Recipes
    • View All Resources
    • GTM Community
    • GA4 community
  • Services
  • About
    • About
    • Contact
  • Login

September 2, 2025

Track Single Page Apps with Google Analytics 4 and Google Tag Manager

Updated: September 2nd, 2025. 

Tracking page views on regular websites is fairly easy: you add a tracking code to every page, and done! Whenever a visitor clicks an internal link, a browser window refreshes, and a new page view event is sent to Google Analytics.

But there are more nuances regarding single-page websites (or web applications). Even though Google Analytics 4 now has a built-in tracking of such pageviews, it doesn’t always work out of the box. And some websites are just really inconvenient from the web tracking standpoint.

The result? Only the first page view is tracked. All the subsequent pages are not captured (until the visitor completely refreshes the page).

You must do additional configuration to track such websites/web apps to mitigate this. But don’t worry; I will show you three methods to track single-page applications with Google Analytics 4 and Google Tag Manager). This is also known as virtual pageview tracking with GA4 and GTM.

Why does this happen? Let me show you.

Subscribe and Get the Ebook - working with reports in ga4

Table of Contents

Here’s what you will learn in this article

  • What is a Single Page App (SPA) / Website?
  • Install Google Tag Manager
  • Multiple ways of tracking SPA + How to read this guide
  • Do you need this guide at all?
  • Does the page URL change (when you navigate from page A to B)?
  • Method #1. Track page views with GA4 Enhanced Measurement
  • Method #2. Try the History Change trigger
    • How many History events do you see?
    • Inspect the dataLayer.push and create variables
    • Sending data to Google Analytics
    • Event tag for the page_view event
    • Google Tag with the “update” parameter
    • Fire the “update” tag before the page_view tag
    • Should Google tag automatically track the first page_view?
    • The summary of the History change setup
    • Test the setup
  • Method #3: With Developer’s help
    • Variables
    • Trigger
    • Create a GA4 event tag
    • Google Tag with the “update” parameter
    • Fire the “update” tag before the page_view tag
    • Should Google tag automatically track the first page_view?
    • The summary of this chapter
    • Test the setup
  • Check the DebugView
  • Conclusion

 

Video tutorial

In addition to this blog post, I have also recorded a video tutorial on my Youtube Channel.

 

What is a Single Page App (SPA) / Website?

Single Page Application (SPA) is a web application or website that loads all of the resources required to navigate throughout the site on the first page load. As the user clicks links/buttons and interacts with the page, subsequent content is loaded dynamically (and the page technically does not reload, even though it might look different).

In older versions of Google Analytics, this caused problems because all of the necessary tracking is loaded once, and the page does not reload during the entire user session. That is why we only got one page view in reports. The URLs might change, but that is to give you the perception of separate pages.

As for Google Analytics 4, Enhanced Measurement offers built-in pageview tracking in such situations. However, that does not work on all single-page websites. Thus, some additional GTM configuration might be needed.

If the term “single page application” does not ring a bell, maybe names like React, Angular, Vue.js, and Gatsby.js are familiar? Websites built with these (or similar) JavaScript frameworks are single-page applications.

Continue reading, and I’ll show you how deep the rabbit hole goes.

Install Google Tag Manager

If you haven’t yet, install the Google Tag Manager on a single-page web app or website. I have published a blog post with several options for your consideration. Also, check whether your Google Tag Manager is working properly.

Done? Great, let’s proceed.

 

Multiple ways of tracking SPA + How to read this guide

There are three techniques explained in this guide:

  • Method #1. built-in tracking of Google Analytics 4 (this is the quickest one)
  • Method #2. Working with a History Change listener (within GTM)
  • Method #3. asking a developer to push pageview data to the Data Layer every time a visitor navigates from one page to another.

But before we jump right into the configuration part, we first need to evaluate the context and find out which tracking option to choose. Just like in my other popular blog post about form tracking, to make complex things a bit more understandable, I’ve created a flow chart that can help you decide which tracking method to choose (or, in many cases, there will be only one method possible for you).

This flowchart might look complex at first, but its goal is to save you time by helping you quickly diagnose how your particular website works. Since SPAs can be built differently, there is no single solution that works for all of them. By answering a few simple “yes” or “no” questions about what you see when you browse your site, this chart will guide you directly to the most appropriate tracking method for your situation, preventing you from wasting time on a solution that will not work.

Note: If all three tracking options are possible for you, you should go with Method 3: developer’s help + dataLayer.push (as it’s proven to be the most robust).

First, let’s take a quick look at it, and then we’ll dive deeper into each step.

google analytics 4 spa tracking - workflow

 

Do you need this guide at all?

The first diamond in the flow chart above asks: When you navigate between pages, do you see new pages in the GTM preview mode?

What does this question mean? By default, the Google Analytics tag fires every time a page is loaded (meaning that the page must load entirely from scratch).

Together with the page’s code, the Tag Manager JavaScript snippet is also loaded from scratch. So if you navigate from page A to B, you will see a new page title here. Every time this appears, it means that the page was completely (re)loaded.

So if you notice a page title on the left side of the sidebar on every pageview, you are not working with a single-page application. Read my GTM guide for beginners instead. I’ve explained there how to track your first GA pageview with Tag Manager.

On the other hand, if you’re really working with a SPA, every time you navigate from page A to page B, you won’t see the page title. Why?

That’s because in web tracking, “pageview” usually is when the page document is loaded, and JavaScript tracking codes are loaded together with it (including Pageview tags). However, in SPA, all the subsequent “pageviews” happen dynamically without loading/reloading the page document. That’s why by default, Tag Manager does not track “dynamic” pageviews.

Because of this, you need to do some additional configuration (and maybe even with the help of a developer).

The reason the standard setup fails is that Google Tag Manager’s default pageview trigger is designed to fire only once, when a page initially loads in the browser. On a traditional website, this works perfectly because every new page view is a complete reload. In an SPA, however, that initial page load only happens a single time. As you navigate between different sections, GTM’s basic pageview trigger never gets a chance to fire again, making it blind to all of these “virtual” page changes.

Alright, so the first question is answered. If you are indeed working with a SPA, let’s move to the next question in our flow chart.

Subscribe and Get the Ebook - Server-side tagging

 

Does the page URL change (when you navigate from page A to B)?

You must ask the developer for help if the URL does not change. Immediately skip to the chapter “Track SPA with GTM and developer’s help” of this blog post and continue reading from there. If the URL indeed changed, you still have chances to use the History Change listener on your own, or maybe the built-in GA4 functionality will take care of this automatically.

URL changes might look different based on the website you’re on. On some websites, the URL fragment changes (that’s the part of the URL after the hashmark #). On others, the URL change might look casual, like on a regular website (e.g., from /home/ to /home/contact-us), etc.

If you confirm that the URL changes, let’s move to another step.

 

Method #1. Track page views with GA4 Enhanced Measurement

Note:  Enhanced measurement provides a bunch of event-tracking capabilities, but many of them can work inaccurately (e.g., scroll tracking). Thus, it will make sense to disable all the non-pageview-related tracking (scroll, site search, click tracking, etc.).

Let’s see if the built-in page view tracking in GA4 works for you.

In Google Analytics 4, go to Admin > Data Streams and click on your web data stream.

Then click the gear icon next to Enhanced Measurement (also, make sure that Enhanced Measurement is enabled).

Then click Show advanced settings under Page views and check if the Page changes based on browser history events checkbox is enabled. This will allow GA4 to automatically try every time the URL changes (or at least try to do so).

After that, save all the changes in GA4. In Google Tag Manager, enable the Preview and Debug mode and then navigate to your single-page application/website. Try navigating from one page to another.

In the preview mode of GTM, you should start seeing History Change events, and their technical event name is gtm.historyChange-v2. You will see it while having the GTM container selected at the top of the preview mode.

If you see that, click on your GA4 measurement ID at the top of the preview mode and check if you see multiple Page View events being sent to Google Analytics 4. If yes, then it’s good news. GA4 will be able to track pageviews of your single-page application automatically, and you don’t need any additional configuration.

Also, check the DebugView of GA4 to make sure that page_view events are coming with proper page_title and page_location parameters.

If you cannot see the History Change events (or the page_view events are not being sent to GA4), then go to the next chapter of this blog post.

 

Method #2. Try the History Change trigger

The most robust solution for tracking single-page applications is cooperation with a developer. Without a doubt. However, sometimes the developers are unavailable, so we might have to work with what we have. That’s why this method is also available as an alternative.

This trigger is GTM’s built-in solution for tracking “virtual pageviews” in many modern web applications. It is designed to listen for changes in the browser’s history, which is what SPAs often use to update the URL in the address bar when you navigate without a full page reload. When the “History Change” trigger detects this type of navigation, it creates an event in GTM that we can use to fire our page_view tag, effectively capturing the page change that a standard setup would miss.

In Google Tag Manager, go to Triggers > New > History Change and create a trigger with the following settings:

Then we need to test if it works. Enable Google Tag Manager Debug mode. Click the Preview button in the top right corner of your GTM interface (near the Submit button).

Once you click the Preview button, a new browser tab will open with tagassistant.google.com. If it does not, read this guide.

A pop-up will ask you to enter the URL you want to test and debug. It might be a homepage’s address or a specific page’s URL, and then press Start.

A new browser tab (or window) should appear where you will see the URL you entered in the previous popup. At the bottom of that page/tab, you must see the following badge:

Click (or scroll) through various sections of the single-page web app (or website) to change the URL. After the change happens, look closely at the event stream in the Preview and Debug console.

Did the History event appear? If yes, that’s good news! If not, skip to the chapter where I explain how to cooperate with developers.

On some single-page applications, you might see several History events occur simultaneously. That’s why you need to answer one more question.

 

How many History events do you see when you navigate from Page A to Page B?

Some websites may be coded in a particular way that will cause multiple history events to appear simultaneously in the GTM preview mode. If you face this situation, read this guide, where I explain how to configure your trigger to work only with certain History Change events (and thus avoid duplicates). Once you read that, come back to this guide.

 

Inspect the dataLayer.push (of a history event) and create variables

When you see the History event in the GTM preview mode, click it and then expand the API call (to see the content of the dataLayer.push).

The most important things for us are:

  • The current URL
  • And the previous URL

In the example below, they are available as gtm.oldUrl and gtm.newUrl.

Let’s create data layer variables for them. If your dataLayer structure is different, you’ll need to adapt.

Go to Google Tag Manager > Variables > Variable Configuration > New > Data Layer variable and enter “gtm.oldUrl”.

Save the variable. Do the same thing for the gtm.newUrl.

We’ll need these variables in the next chapter.

 

Sending data to Google Analytics (with the History Change trigger)

After you make sure that the History Change trigger works fine, you will have to send those pageviews to Google Analytics.

If you choose this route, first, disable History event tracking in GA4. You can do that by going to Admin > Data Streams > Select your website stream > Enhanced Measurement > and disabling “Page changes based on browser history events.”

Important side note. On some websites, you might notice that your single-page app’s pageviews are tracked properly, BUT in the reports, you might notice that the title of the tracked page always belongs to the previous page. If you notice this, you will need to implement single-page app tracking with the developer’s help, or you can delay the History Change event.

Subscribe and Get the Ebook - Increase data accuracy in google analytics 4

Event tag for the page_view event

Google Analytics 4 is an event-based analytics platform. It means that everything is an event. Pageview, click, purchase, etc. Every time a history event happens, we’ll need to send a page_view to GA4.

In Google Tag Manager, go to Tags > New > Google Analytics > GA4 event tag and enter the following settings:

Your Measurement ID should be different. In this tag, I added two event parameters:

  • page_location (its value is the gtm.newUrl data layer variable)
  • page_referrer (its value is the gtm.oldUrl data layer variable)

If you are using a Google Analytics 4 event settings variable, then include these parameters there.

If your page URLs contain a hashmark (#), then you might need to create different variables (because gtm.newUrl and gtm.oldUrl don’t include #). A solution for the current URL could be a JavaScript variable called window.location.href.

You can use that as the value of page_location.

Finally, assign the History Event trigger to the GA4 event tag (page_view).

Note: if you are not using a GA4 event settings variable, you must override page_location and page_referrer manually in all GA4 event tags.

 

Google Tag with the “update” parameter

By now, you should have two tags directly related to the single-page application tracking:

  • The page_view event tag
  • And the Google Tag (that you were supposed to have even before starting to read this article). That’s how you should have installed GA4.

An example of a Google Tag might look like this:

Yours might have more customization. Mine is just the bare minimum. The basic stuff. Keep your tag as it is.

But when the page’s URL changes, you will have to inform the Google Tag that there were some updates.

This can be done by copying your main Google Tag and adding the following changes (keep other original settings unmodified):

  • update means that Google tag will be informed about changes (no additional page_view will be sent by this tag). This is needed for automatically tracked events, such as user_engagement. Together with this command we will send what kind of changes happened:
    • page_location value changed
    • page_referrer also changed

This is needed to avoid a phenomenon called rogue referral. This will help with the attribution. Even though, at one point, Google announced that they are handling this automatically, I don’t trust them 🙂 I was let down too many times; thus, I will handle it myself.

IMPORTANT: This tag should not have a trigger. Save this tag without it (soon, you’ll learn why).

 

Fire the “update” tag before the page_view tag

Go back to the page_view event tag and then Advanced settings > Tag Sequencing > Fire a tag before GA4 page_view > select the “update” tag you recently created.

Save the tag.

 

Should the Google tag automatically track the first page_view?

By default, when the Google Tag fires, it sends the page_view to GA4. However, on some single-page applications, this might be a source of duplicate pageviews.

What should you do? Check what happens after you load/reload the page of your single-page application.

Does the History event appear in the preview mode every time you reload the page?

If you reload the page (without clicking anywhere else on your website) and you see this:

…then you should disable the automatic pageview tracking in the Google Tag.

If you already have the send_page_view=true, then change its value to false.

However, if History events don’t appear immediately after you refresh the page and they become visible only when you start navigating your website/web app, then don’t modify the Google Tag.

 

The summary of the History change setup

I understand that some steps in this guide can be confusing (because SPA tracking with GA4 is not an easy feat). Here’s what you should have:

  • A regular Google Tag (that you were supposed to have before reading this guide). We did not do any changes here
  • A history change trigger that fires when the URL changes. If you see multiple history change events, read this and make your trigger more precise.
  • A GA4 event tag that sends the page_view event to GA4.
    • Together with the event, we also send page_referrer and page_location. page_referrer is necessary if you want to avoid potential attribution issues.  In my example, I used gtm.oldUrl and gtm.newUrl data layer variables but your situation might require using something different. What’s important is that newUrl shows the correct URL (including URL fragment (#) and URL parameters (e.g., UTMs) and the oldUrl at least shows the correct domain of the previous page.
    • This tag should use the History Change trigger.
  • In the page_view event tag, you should configure tag sequencing. Before this tag fires, a Google tag (with “update” parameter) should be activated.

 

Test the setup

After you configure and save everything, it’s time to check the setup. I briefly explain how to test your page_view tracking here.

 

Method #3: Track Single Page Applications with Google Tag Manager and Developer’s help

Most likely, you’ve reached this point because your History Change tracking attempts failed, or you are simply curious to learn more.

If (for some reason) the History Change trigger isn’t working for you (or there was another reason which brought you to this section), there’s another option on how to track single-page web app with Google Tag Manager.

Ask a developer to activate a dataLayer.push code whenever a user navigates between pages/states of a website/web application.

Here’s a sample code snippet that the developer could use:

<script>
 window.dataLayer = window.dataLayer || [];
 window.dataLayer.push({
 'event': 'virtualPageview',
 'pageUrl': 'https://www.mywebsite.com/something/?page#contact-us',
 'pageTitle': 'Contact us' //some arbitrary name for the page/state,
 'previousUrl': 'https://www.mywebsite.com/something-else/'
 });
</script>

Note: Values of ‘pageUrl’, ‘previousUrl’, and ‘pageTitle’ parameters (in that code snippet) should be dynamically replaced with the URL (and title) of the page a visitor is currently viewing. A developer should take care of that. If the URL contains the # or question marks and some parameters, they should be included there too. If the URL contains some query parameters (e.g., UTM parameters, they should also be included).

Anyway, what does that code mean?

Whenever a user goes to a particular section of your page, a developer should activate that little bit of JavaScript code. It indicates that a “virtualPageview” has occurred, and the new page URL is https://www.mywebsite.com/something/?page#contact-us. 

Then, you will use this dataLayer.push as a triggering condition in GTM (that activates the GA Pageview tag) and then send the title and page path (page over to GA.

To achieve this, complete the following steps:

  • Create three variables (and include them in the GA4 event tag). If you don’t have the pageTitle, then create two variables. At least have pageUrl and previousUrl.
  • Create a trigger (and assign it to the GA4 Event tag).

 

Variables

  • Title: dlv – pageUrl (dlv stands for “Data Layer Variable”)
  • Variable type: Data Layer Variable
  • Data Layer Variable Name: pageUrl
    This variable will read the value of the pageUrl that was pushed to the Data Layer by a developer.

Create a 2nd variable:

  • Title: dlv – pageTitle 
  • Variable type: Data Layer Variable
  • Data Layer Variable Name: pageTitle

Create a 3rd variable:

  • Title: dlv – previousUrl
  • Variable type: Data Layer Variable
  • Data Layer Variable Name: previousUrl

 

Trigger

  • Title: Custom – virtualPageview
  • Type: Custom Event
  • Event name: virtualPageview
  • This Trigger fires on: All Custom Events

 

Create a GA4 event tag

Go to Tags > New > GA4 event tag and enter the following configuration.

This time, I override three parameters (page_location, page_referrer and page_title). Their values are the Data Layer variables that I created in one of the previous chapters. Important: you will also need to do that in all other future GA4 event tags.

Set this tag to fire on a Custom Event trigger that you have also recently created.

 

Google Tag with the “update” parameter

By now, you should have two tags directly related to the single-page application tracking:

  • The page_view event tag
  • And the Google Tag (that you were supposed to have even before starting to read this article). That’s how you should have installed GA4.

An example of a Google Tag might look like this:

Yours might have more customization. Mine is just the bare minimum. Keep your tag as it is.

But when the page’s URL changes, you will have to inform the Google Tag that there were some updates.

This can be done by copying your main Google Tag and adding the following changes (keep other original settings unmodified):

  • update means that Google tag will be informed about changes (no additional page_view will be sent by this tag). This is needed for automatically tracked events, such as user_engagement. Together with this command we will send what kind of changes happened:
    • page_location value changed
    • page_title was modified
    • page_referrer also changed

This is needed to avoid a phenomenon called rogue referral. This will help with the attribution. Even though, at one point, Google announced that they are handling this automatically, I don’t trust them 🙂 I was let down too many times; thus, I will handle it myself.

IMPORTANT: This tag should not have a trigger. Save this tag without it (soon, you’ll learn why).

 

Fire the “update” tag before the page_view tag

Go back to the page_view event tag and then Advanced settings > Tag Sequencing > Fire a tag before GA4 page_view > select the “update” tag you recently created.

Save the tag.

 

Should the Google tag automatically track the first page_view?

By default, when the Google Tag fires, it sends the page_view to GA4. However, on some single-page applications, this might be a source of duplicate pageviews.

What should you do? Check what happens after you load/reload the page of your single-page application.

Does the History event appear in the preview mode every time you reload the page?

If you reload the page (without clicking anywhere else on your website) and you see this:

…then you should disable the automatic pageview tracking in the Google Tag.

If you already have the send_page_view=true, then change its value to false.

However, if History events don’t appear immediately after you refresh the page and they become visible only when you start navigating your website/web app, then don’t modify the Google Tag.

 

The summary of this chapter

I understand that some steps in this chapter can be confusing (because SPA tracking with GA4 is not an easy feat). Here’s a summary:

  • A regular Google Tag (that you were supposed to have before reading this guide). We did not make any changes here.
  • We asked the developer to push virtualPageView event to the data layer when the page URL changes. Together with it, we asked to push pageUrl, pageTitle, and previousUrl. We created a trigger and 3 variables for that
  • A GA4 event tag that sends the page_view event to GA4.
    • Together with the event, we also send page_title, page_referrer and page_location. page_referrer is necessary if you want to avoid potential attribution issues.  In my example, I used pageUrl and previousUrl data layer variables. What’s important is that pageUrl shows the correct URL (including URL fragment (#) and URL parameters (e.g., UTMs) and the previousUrl at least shows the correct domain of the previous page.
    • This tag should use the History Change trigger.
  • In the page_view event tag, you should configure tag sequencing. Before this tag fires, a Google tag (with “update” parameter) should be activated.

 

Test the setup

After you configure and save everything, it’s time to check the setup. I will briefly explain how to test your page_view in the next chapter.

 

Check the DebugView

Save all the changes in your container and enable/refresh the preview mode by clicking the Preview button in your GTM container. Then go to the GA4 DebugView (you will find it at Admin > DebugView).

When you load the first page of your single-page app, you should see one page_view event in the DebugView.

Check whether page_location and page_title parameters are correct and belong to the current page you are looking at.

Then start navigating your website/web app and check if the number of page views that you see in the debugview is correct and whether their parameters are correct.

Within the next 24 hours, you should start seeing your pageview data in Reports > Engagement > Pages and Screens reports of GA4.

If the GA4 debugview is not working for you, then take a look at this troubleshooting guide.

 

Track Single Page Web App with Google Tag Manager: Conclusion

The problem with single-page web apps or websites is that regular pageview tracking does not work. All necessary code is loaded once, and the page does not reload during the entire user session. In some cases, GA4 might be able to track pageviews of single-page applications, but it does not cover all situations.

With certain configurations in Google Tag Manager (and possibly some input from your developer), you can still track them. In this blog post, I’ve explained the built-in GA4 solution and two other options on how to track single-page websites: use GTM’s built-in History Change trigger or cooperate with a developer.

If you’re not sure which method to choose, here’s a rule of thumb:

  • If you have development resources, cooperating with a developer is a more robust option (in terms of tracking quality).
  • But if those resources are unavailable right now, check the flow chart I’ve included at the beginning of this guide. However, there’s still a chance that, eventually, you’ll end up asking for the developer’s input.

If you found this post about tracking a single-page web app with Google Tag Manager useful, consider subscribing to my newsletter.  You’ll get various bonuses and useful stuff related to GTM.

Subscribe and Get the Ebook - Server-side tagging
Julius Fedorovicius
In Google Analytics Tips Google Tag Manager Tips
46 COMMENTS
Joe Privett
  • Feb 14 2021
  • Reply

As part of new PCI compliance laws coming in, it will be necessary to exclude scripts on 'payment' pages. How can we exclude GTM specifically on payments pages within an SPA?

    Julius
    • Feb 14 2021
    • Reply

    You should discuss this with your developers.

Omar Dahroug
  • Feb 1 2022
  • Reply

Hi Julius, thanks for your blog. I'm currently building a next.js application and would like to load GTM only AFTER the entire page is loaded. The goal behind this is to improve the website performance. Have you figured out a way how I can run third-party JS code only after my application has loaded?

    Julius Fedorovicius
    • Feb 1 2022
    • Reply

    No,that is developer's responsibility to figure out

    Peter Svegrup
    • Oct 8 2023
    • Reply

    I'm late to the party but just in case this can be useful; checkout out Partytown that uses service workers to load your tracking scripts off the main thread.

Andria
  • Apr 6 2022
  • Reply

Hello and thank you for the great article.

We have a SaaS product which is also a SPA and each time a user requests a free trial of the product, we create an instance of the following format:
username.domain.com

So apart from our website that we are tracking as domain.com we want to also be able to track activity on each one of the SPA accounts created.. so *.domain.com

Is this possible and how can we do it?

    Julius Fedorovicius
    • Apr 13 2022
    • Reply

    GA automatically handles subdomains. Just make sure that you use the same GA property/data stream for every subdomain.

Eunice
  • Aug 23 2022
  • Reply

Hi Julius, Thank you for going into such detail and I always appreciate your flow charts :)

According to GA4 documentation, the configuration tag should be fired on every page. I assume this includes virtual pages. Is there anything wrong with having both history changes and page views as triggers for the config tag (when enabled to fire a page view event)? Or is it better to create a separate GA4 event for history changes as you described?

    Julius Fedorovicius
    • Aug 24 2022
    • Reply

    It does not include virtual pages

Dennis G Allard
  • Feb 16 2023
  • Reply

"GTM" is used early in this article before the term is defined. In fact, the term is never explicitly defined. It is pretty obvious that "GTM" means "Google Tag Manager". Nevertheless, the first use of "GTM" should be defined when it is first used in the article.

    Julius Fedorovicius
    • Feb 16 2023
    • Reply

    One of the early chapters in this blog post is called "Install Google Tag Manager" where I provide additional links to get started with it. Also, blog post's title is called "Track Single Page App with Google Analytics 4 and Google Tag Manager" which should set the expectations from the beginning.

Milena
  • Feb 27 2023
  • Reply

Hi, Julius,

Thank you for the amazing article, it really helped me a lot to understand SPAs better.

I have though a question regarding the GA4 configuration tag (where you are mentioning the "Send a page view event when this configuration loads" checkbox). Are you referring to the same tag that is created when setting up GA4 or it is a second GA4 configuration tag, that I should create?

Thank you once again for the valuable input!

Best regards,
Milena

    Julius Fedorovicius
    • Feb 27 2023
    • Reply

    It's the same tag

Will Chou
  • Jun 13 2023
  • Reply

In a situation where the SPA site is having an issue with the query paramater of GTM Preview Mode what do we do? We can't use PReview mode to diagnose this via the workflow. We're working on seeing if we can update the SPA site somehow. What's happening is it's a multi-page form and one cannot click the big button to go to the next page unless we take off the preview mode query param, which effectively stops Preview Mode from capturing. Any way around this? i.e., testing for this stuff in Dev tools or GA4 interface?

    Milena
    • Jun 14 2023
    • Reply

    Hi, Will Chou,

    I am not sure if that will help you, but if you refer to "gtm_debug=" signal which attaches to the URL of your website when you enter Preview mode, you can disable it by deselecting the "Include debug signal in the URL" option, which is enabled by default. You will find it right after you click on the Preview button under "Your website’s URL field”.
    I hope this will help. 😊

    BR,

      Will Chou
      • Jun 16 2023
      • Reply

      This solved the issue. Thanks so much!!

Ritvik Sharma
  • Jul 29 2023
  • Reply

Hi Julius,
If the Config tag only loads once for SPAs, how do you propose we use the Config tag as the equivalent of the GA3's settings variable? Do we have to fill the same event parameters for all GA4 event tags? If so, that sucks and Google must do something about it.

    Julius Fedorovicius
    • Aug 3 2023
    • Reply

    GA4 config tag is not the same as GA3 settings variable. So yes, you should set parameters in all tags.

Nico
  • Jul 29 2023
  • Reply

Thanks for the enlightening article. I have read and applied the knowledge. Now I'm a bit confused how to implement this for conversion linker from Google Ads.

About the firing triggers/triggering, which one should I use? A or B or both? It's for tracking the leads conversion.
(A) Page View (normal version)
--Page URL contains thanks?status=success

(B) History Change
--historySource contains pushState
--Page URL contains thanks?status=success

Please advise. Thank you.

    Julius Fedorovicius
    • Aug 3 2023
    • Reply

    Conversion linker should be fired on all pages. That's it.

      Lorenzo
      • Nov 15 2024
      • Reply

      Hi Julius, talking about Conversion Linker in SPA, is it:
      - All pages (meaning only once besides reloads) OR
      - All virtual pages?

        Julius Fedorovicius
        • Nov 15 2024
        • Reply

        All pages

Victoria
  • Aug 16 2023
  • Reply

Hi Julius - if I use History Change trigger to track the single page app pageviews, should I disable the 'Page changes based on browser history events' checkmark in the Enhanced Measurement in GA4?

Saurabh
  • Aug 31 2023
  • Reply

Hi Julius,

Amazing detail as usual. Love it!

Regarding disabling 'Send a page view event when this configuration loads'

I have in fact have configured a virtual custom event to fire when SPA and its subsequent steps load. And this fires even when the the page is reloaded. Based on this I am would need to disable the 'send a page view' checkbox on the main GA4 property setup.

Once disabled, how does GTM send pageviews from all of of the other non-SPA pages on my site? Isn't that the purpose of that feature?

Thank you!

Mike
  • Sep 12 2023
  • Reply

This is really useful for pageviews - is there a similar way to track outbound links from a single page web app? I can see the history change in GTM debugger, but as soon as I click an outbound link this isn't logged.

alunet
  • Sep 30 2023
  • Reply

Could you please explain this sentence
" Link this tag to the aforementioned GA4 configuration tag"

How to link GA4 config tag to GA4 page_view tag? Do you mean using "Tag Sequencing" or "Tag firing priority" in GA4 page_view tag?

E
  • Oct 19 2023
  • Reply

I have this set up using the developer method. It works perfectly in debug preview mode, but when I publish, the virtual pages on the form break and rather than going to the next one, it reverts to previous page. This seems to be caused by the GA4 GTAG. Any ideas?

Stefan
  • Nov 21 2023
  • Reply

Hi Julius,

Great blog. What do you recommend as 'Tag firing option' for the GA4 page view event tag for SPA's: once per event or once per page? And why?

I've implemented it via method 3 with help of DEV, so we fire a custom 'virtual_page_view' event on every 'page'.

Already checked if it always loads when refreshing the page and it does, so I've set the 'send_page_view' to 'false' in the GA4 config tag.

Curious!

Stefan

Jess
  • Nov 30 2023
  • Reply

Hi Julius,

The flow chart at the start of this was so good I had to stop reading to double check if I was right in thinking my organisations site was an SPA (something I'd only learnt about 40 mins before reading this as I've been having issues with tags not firing until a page is refreshed).

I double checked what page views could be seen as it stuck out to me that the flowchart says if new page views are created it's not an SPA- I knew that half the pages (e.g. the main 'news' section but not the individual news articles) caused a new page view!

I believe it is possible to have hybrid approach, our developers talk about the sections of the sites as 'apps' that do seem to all be their own unique thing and their company offer a variety of 'apps' we can use on our site. So I believe these sections/apps may be of an SPA style within a more traditional site.

I will be reaching out to them to clarify this and ask for support in implementing something so I can track user activity and page views more consistently. But do you happen to know if the methods you list here work well when the site is sort of...hybrid? Or will I end up with issues on the more traditional parts of the site that currently work okay? (I have better luck when I go to them knowing exactly what fix I need implemented and don't want to suggest one that will cause more issues- I'd rather go with no suggestion).

    Jess
    • Jan 10 2024
    • Reply

    All sorted now :)
    Thanks again for the guides you publish!

Sunny
  • Jan 8 2024
  • Reply

Hi,

Thank you for this guide and I have followed the exact steps for data layer to send custom page_titles.
I am still getting the browser titles being passed. How can I stop from google sending the browser titles? also, GA4 Tag Config should be triggered on all pages or the customevvirtualpageview event?

Nick Martin
  • Jun 10 2024
  • Reply

Hi Julius,

I'm working on a site where GA4 has been added by developers via code, but we have the Google Tag within a GTM container. The site is an SPA.

I'm seeing GA4 firing on every page load and working as it should and can see that "page changes based on browser history events" is checked in the GA4 setup , but Google Tag only fires on the initial site load, meaning that any event tags I have set up are not firing on any subsequent pages a visitor navigates to as the Google Tag is not loading.

In this situation would moving the GA4 tag into the GTM container fix the issue, or do we use your method above of measuring history change triggers?

Many thanks

Srilaxmi
  • Sep 20 2024
  • Reply

Hi Julius,

Thank you for the insightful article! I’m currently facing an issue with a React SPA where I’m updating the page titles dynamically based on backend responses. I need to manually push pageviews to Google Tag Manager (GTM) rather than relying on automatic tracking.

I’ve initialized GTM in the index.html like this:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'gtag.init',
'gtag.config': {
trackingId: 'G-4CG75M28S5',
send_page_view: false // Disable automatic page view tracking
}
});
</script>
<!-- Google Tag Manager -->
<script>
(function (w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({
'gtm.start': new Date().getTime(),
event: 'gtm.js'
});
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s),
dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', 'GTM-KC67Z3SM');
</script>
<!-- End Google Tag Manager -->

And I’m attempting to manually push pageviews using:

useEffect(() => {
window.dataLayer.push({
'event': 'gtag.init',
'gtag.config': {
trackingId: 'my_id',
send_page_view: false
}
});
}, [window.location.pathname]);

However, GTM automatically starts tracking pageviews once initialized, and when I navigate between screens, multiple page_view requests are fired. I need to stop this automatic tracking and ensure that pageviews are only pushed manually after the title is set.

I’ve been stuck on this for over a week. Could you provide any guidance on how to resolve this?

Thanks in advance!

    Julius Fedorovicius
    • Sep 20 2024
    • Reply

    Disable automatic pageview tracking in enhanced meaurement. Also, when you launch gtag, set send_page_view to false

      Srilaxmi
      • Sep 20 2024
      • Reply

      Hi Julius,

      I’m facing the same issue. Even though I’ve set send_page_view: false, I’m still seeing tracking logs for page initialization, DOM load, and navigation events.

      Do you have any suggestions on why this might be happening and how to prevent it?

      Thanks!

      Srilaxmi
      • Sep 20 2024
      • Reply

      Hi Julius,
      By default page_view tracing in enhanced meaurement is enabled and its not able to edit what can i do .

Chavdar Dimov
  • Sep 22 2024
  • Reply

Hi Julius,

I have this case where a part of the website is on Symphony technology (home page, index pages) but the articles are o next.js which is single page application. So it's a struggle how to measure it. Currently the page views are being tracked thanks to the enhanced measurement of ga4, but the data layer doesn't go to GA.

Siva
  • Sep 27 2024
  • Reply

Hello Julius,

we are having a salesforce experience cloud page and the behaviour is:

upon clicking the links (say inbox), the base url remain same, but the end of the url changes like : /s/inbox (effectively a page load).

once you navigate to inbox, there is a sidebar on the page with link that are specific inbox related items says product Messages, security Message etc. upon clicking the link, there url path will not change, but url parameter are added /s/inbox?product. This is the SPA scenario where the full page is not loaded.

with GA4 enhanced measurement page_views is enabled and the both the flags
page loads and page changes based on browser history changes are ticked.

in the events section, we see Inbox and other links that cause the page url changes are listed.

the issue is: when we click on Inbox, we are receiving double page_views in quick succession. But when we click on secure Messages (side bar) with in Inbox, the browser history changes are causing the datalayer.push and so only one page_view is logged.

google tag manager is not used in the company as it is not approved as google analytics support ream.

there is a gaEvent that is sent from salesforce.

Also on some salesforce sandbox, we see googletagManager and googleanalytics javascripot being loaded. can you please suggest

Regards
Siva

Til
  • Oct 3 2024
  • Reply

Thanks for the guide! I'm just wondering how do you set up e-commerce tracking on a SPA? Should you just exchange the trigger to a history change trigger instead?

tony
  • Dec 3 2024
  • Reply

Hello Julius,
Does that mean that for all other events we should as well load the configuration tag before executing them ? or due to being set with the virtualpageview we should be all set for all events coming after the pageload ?

Georg
  • Feb 12 2025
  • Reply

Do you know how "gtm.newUrl" is actually generated and which value it gets? Is it safe to rely on this variable in the long term or could it break some day? It does not seem to be an official variable, correct?

Tim
  • Mar 6 2025
  • Reply

For my SPA website, I use GA4's automatic pageview detection. This works well for tracking different page views, but not for events on those pages. The time on page for all subsequent pages remains at 0 seconds, while the counter for the landing page does increase. It seems like the events are still being attributed to the first page. Could that be correct? And what would be a good solution for this?

Jamie C
  • Apr 18 2025
  • Reply

Hi Julius,

I've come across your very helpful guide when trying to deploy this for a client website. The only issue I am running into is that when I review GA4's debug mode, I am getting TWO pageview events whenever the GA4 event tag fires (the one that is sending a page_view) event. I have the configuration exactly as you recommended and I have tried several fixes, including pausing each tag in turn to test and telling the 'update' tag not to send a pageview.

I have had developers set up the 'virtualPageView' DL event, so I am not using the history change event as the trigger.

Any help would be greatly appreciated.

Puneet Pandey
  • Jun 25 2025
  • Reply

Hi Julius,

Thanks for sharing this article! I am seeking your guidance in 1 of the issue I am facing with my existing GTM setup.

We have recently migrated our front-end from Ruby on Rails driven pages to NextJS. After this migration, we see a huge drop in automatic collected events like `user_engagement`, `click`, `scroll`.

I used GA4 debug mode and GTM preview mode to verify the same.

There are obvious differences on how page renders on Rails views vs SPA like NextJS.

I tried to set `update` parameter to true for the Google Tag in GTM but that's not helping to generate `user_engagement` event (for example a user spends 10s+ time on a page before navigating).

The original setup I have is:
1. GTM
Tag Type - Google Tag
Triggering - Initialization - All Pages

2. Since my NextJS uses App Router hence page_view events are auto captured using history change events.
Inside GA > Admin > Data Streams > Enhanced Measurement > Page Views both the checkboxes "Page loads" and "Page changes based on browser history events" are checked.

I also noticed that:
a. `user_engagement` event only fires when I close the window
b. When I launch my website `first_visit`, `page_view` and `session_start` events triggers successfully.
c. When I start navigating I do not see `user_engagement` event (even if I fulfils the criteria of this event to trigger)

I also tried to set `send_page_views` to true for Google tag and disable "Page changes based on browser history events" checkbox but that is not helping either.

Please advise what I am doing incorrect here!
Also, is this how default event works in SPA?

I would be happy to provide you more information if needed.

Vivien
  • Aug 29 2025
  • Reply

Hi Julius,

thank you for this great article. Can you tell me how to implement scroll tracking on a Single Page Application?

Thank you in advance!

Kind regards,

Vivien

Sparsh
  • Sep 11 2025
  • Reply

I think what is missing from the blog is about the consent banner. A page_view is not sent until a user accepts the consent. For SPA where the URL does not changes on first load as the page_view is not sent for a new visitor and once the consent is accepted as neither their is a page load or a history change.

Leave a comment Cancel reply

Your email address will not be published. Required fields are marked *


 

Hi, I'm Julius Fedorovicius and I'm here to help you learn Google Tag Manager and Google Analytics. Join thousands of other digital marketers and digital analysts in this exciting journey. Read more
Analytics Mania
  • Google Tag Manager Courses
  • Google Tag Manager Recipes
  • Google Tag Manager Resources
  • Google Tag Manager Community
  • Login to courses
Follow Analytics Mania
  • Subscribe to newsletter
Recent Posts
  • Google Tag Manager Server-Side Tagging with Stape
  • How to Preserve Ad Click IDs with Server-side Tagging
  • LinkedIn Conversions API with Google Tag Manager. The Guide.
Analytics Mania - Google Tag Manager and Google Analytics Blog | Privacy Policy
Manage Cookie Settings