Pixels · engineers · PMs
The URL changed. The pixel did not.
React, Vue, and Next client navigations swap the route in place. GTM's History Change trigger exists because DOM Ready already happened on the first page. A Purchase bound to Window Loaded never sees /thank-you. A CAPI event that reuses the landing event_source_url tells Meta the sale happened on /shoes.
Fire on the route you care about
A page_view or PageView on every route you measure, with page_location and page_path set to the new URL, not the first one. GA4 collect uses en=page_view, dl for document location, dp for path. Meta uses ev=PageView and, on CAPI, event_source_url. If those fields still show https://shop.example/ after the user is on https://shop.example/checkout, every report is a first-page report.
Fire conversion events on the thank-you route, not on a layout that remounts. A layout-level useEffect that calls fbq('track','Purchase') will fire on every child route change. Bind Purchase to the order-complete screen, and only after the order id is real. A loading spinner that shares the thank-you route is a duplicate waiting to happen.
History Change in GTM
A trigger on Window Loaded or DOM Ready will not re-run on client navigation. History Change plus a dataLayer push from the router is the usual pattern. History Change fires on pushState, replaceState, and popstate. If the router uses replaceState for query tweaks (a filter, a modal flag), you will overfire unless you filter on pathname or on a dataLayer event you control.
Container Load still fires PageView on the first document. History Change then fires PageView on the first pushState some routers emit during bootstrap. The first route therefore counts twice. Exclude the first history event, or fire only from the router. GTM preview will show both tags firing; the Network tab will show two /g/collect en=page_view hits. That is not twice the traffic.
React Strict Mode double Purchase
React 18 Strict Mode mounts, unmounts, and remounts in development. If you put fbq('track', 'Purchase') in a useEffect with no guard, dev duplicates. Production may not, which makes the bug undebuggable in prod and over-reported in staging. Guard with a module-level Set of order ids, or fire from the router once when the order id first becomes available.
The same Strict Mode double applies to gtag('event','purchase') and ttq.track('CompletePayment'). An event_id that is a new UUID per effect run will not dedup; you will send two ids for one order. Derive event_id from the order id. Then a double mount still collapses at the vendor if both transports send it, and your finance join still works.
event_source_url is the current page
Meta CAPI requires event_source_url when action_source is website. It must be the page where the event happened, which in an SPA is location.href at fire time, not the first page of the session and not document.referrer. A Purchase with event_source_url of the landing page makes Meta think the sale happened on the ad destination. Domain filters and match quality both suffer.
Populate it from your server if the event is a webhook: store landing URL and thank-you URL separately. The landing URL is useful as a custom property. It is not event_source_url for Purchase. pixellint validate json will flag a website action_source with no event_source_url. It will not know that you sent the wrong path. That is a fixture you write yourself.
SSR and hydration
The first paint of a Next.js or Nuxt page may include a noscript pixel. The hydration pass then fires JS. Deduplicate or you count two pageviews per landing. App Router makes this easy to miss because a server component does not run fbq, and a client component does, and both can emit markup. Fire PageView from one place after hydration, or accept noscript-only for no-JS and JS-only for everyone else, with event_id if both can run.
replaceState during hydration to clean a trailing slash will trip History Change. Filter it. A pixel that fires on every search-param edit will make /checkout?step=1 and /checkout?step=2 look like two pageviews. Decide whether a step is a page. Then encode that decision in the trigger, not in a hope that GA4 will sample it away.
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.