Page views and single-page apps
The rule is one sentence — one real URL change, one page view. This page covers how the tracker enforces it, why you must not write a manual `ap('page')`, and why a `replaceState` onto the same URL must count nothing.
One load, one page view#
The snippet the console generates contains a single command: ap('init', …). That one command does two things:
- It sends the first
page_view. - It installs the History hook, so later routes in a single-page app are counted too.
No other command is needed to count page views — and none should be written.
Earlier versions of the install snippet carried an ap('page') alongside init, and that manual command was exempt from the deduplication rule. The result was two page views per load, and three if the tag appeared on the page twice. Measured against real traffic, every genuine page load was recording more than three page views on average.
The cost was not only the page-view number. Conversions defined by a Create-event rule keyed on the page URL are minted out of each page_view, so the same multiplier landed directly on the order count. In one real case, a page showing twelve views in GA4 showed 296 in AdPix.
The counting rule#
For each page context, the tracker remembers the last URL it sent a view for. The comparison key is path plus query; the hash fragment is deliberately excluded.
| What happens | Page view recorded? |
|---|---|
| First page load | Yes |
| Navigating to another route in a single-page app | Yes |
| A query parameter changes, e.g. page two of a list | Yes |
| Full page refresh | Yes — the page context is new |
replaceState or pushState onto the same URL |
No |
| Only the hash fragment changes | No |
A manual ap('page') for a URL just counted |
No |
All three automatic paths — the first view, the History hook, and the re-fire after consent is granted — go through this one gate. A URL is marked as counted only when the event is actually sent, so a view held back for lack of consent still fires once consent is accepted and is never lost.
Why replaceState onto the same URL must not count#
This is the most important detail on the page. Modern web apps — WHMCS and similar customer panels in particular — call replaceState and pushState for non-navigation state: sorting a table, switching a tab, applying a filter, restoring scroll position, every AJAX request. Almost always with the URL unchanged.
If the History hook fired on every call, all of those would become page views. On a real site this made page-view and engagement counts four to five times GA4's while session and form-submission counts matched exactly — the signature that gives the cause away.
GA4 behaves identically: in its enhanced measurement, "page changes based on browser history events" produces a view only when the URL actually changed. AdPix is aligned with that rule so the two tools' numbers mean something side by side.
The History hook sends the view on the next browser tick rather than at the instant of the call. That short gap gives your framework time to update the URL and the page title, so the event is recorded with the right title.
Hash routing#
If your routes look like #/checkout, changing route does not produce a page view. This is deliberate and matches the GA4 default.
Note that the manual command is not a workaround here either: since deduplication was extended to the manual path, ap('page') sends nothing for a URL whose path and query have not changed. The only correct fix is real History API routes — what every modern router offers by default.
Page views and engagement time#
In a single-page app, every page view that is actually recorded also carries a user_engagement event holding the time spent on the previous route. If the page view is not recorded, that event is not sent either — which is why the engagement counts had inflated by exactly the same ratio.
That number is what the reports read for average session duration. Event time is stamped on the server and is not usable for measuring duration, so any error in page-view counting lands directly on the engagement figures too.
What to do#
ap('page') line, remove it and replace the whole block with the current version.ap('track', …); that command has no deduplication and sends every time.If the count is still high#
Three independent guards now prevent this error: the current snippet no longer carries the extra command, the tracker deduplicates the manual path too, and the collector collapses duplicate views inside a single delivery batch. A few cases still remain:
- An old tracker is still cached in browsers or at the CDN. The tracker file is cached for roughly a day; after that, even a page carrying the old snippet corrects itself. Do not wait for it — replace the snippet.
- The tag is on the page twice. The tracker initialises only once per measurement ID per page, so the second copy adds no views; but having two install routes is still wrong and should be reduced to one.
- Conversions built on the page URL. Every time the "payment successful" page loads — including on a refresh or a back navigation — another conversion is minted. The view count is correct, but "the success page loaded" is not the same thing as "an order". Counting orders accurately needs the order ID, and the best way to send it is server-side.
The symptoms of each case and how to tell them apart are in Page views counted twice.
Frequently asked questions#
My old snippet has an `ap('page')` line. Should I remove it?
Yes. That line counted every page view twice and was the root cause of the multi-counting. Both the tracker and the collector now neutralise the duplicate, but the correct fix is to delete the line and replace the whole block with the current snippet the console generates.
My site uses hash routing — does nothing get counted?
A hash change on its own does not produce a page view. That is deliberate parity with GA4. A manual page command does not help either, because counting is keyed on the URL's path and query, and neither changes under hash routing. The fix is real History API routes.
Why is my count higher than GA4's?
Almost always one of three things — an old snippet with the extra page command, a tag installed both in the site template and in a tag manager, or a conversion defined by a Create-event rule keyed on the page URL, which mints another one on every repeat view.
Does a refresh count as a new page view?
Yes. A refresh means a completely fresh page context, so the "last counted URL" memory is empty and the view is recorded again. What is not counted is a state change inside the same page.
Thanks — your feedback helps us improve the docs.