Custom website tracking

Custom Website Conversion Tracking: React, Next.js, Vue & HTML

This is for businesses on a custom-built site, whether a React, Next.js or Vue app, a single-page app, or hand-coded HTML, who need GA4 and ad platforms to see the right conversions. There's no plugin to lean on, so tracking only works if the site and GTM agree on what happens and when. I write that agreement as a dataLayer spec, build the GTM side, and add server-side events for the conversions that matter.

Custom website · Ecommerce & lead gen
  • Page views that follow client-side route changes without duplicates
  • A dataLayer spec your developers can implement and test
  • Conversions fired when your app confirms them, not on clicks
  • Server-side GA4, Meta CAPI and Google Ads with shared event IDs
Quick answer

Proper tracking on a custom site or SPA means GTM loaded once in the app shell, page views sent on every route change but only once, a written dataLayer contract for ecommerce and lead events pushed after your code confirms success, and server-side events from your backend for payments and sign-ups, deduplicated against the browser with a shared event ID.

Tracking flow

How tracking works on Custom website

The events I set up on Custom website, and where each one is sent.

Why it breaks

Why Custom website tracking goes wrong

Route changes don't send page views

In a single-page app the URL changes without a full page load. GTM's page-view trigger fires once, so GA4 sees one page per visit and funnels look empty.

Or every page is counted twice

GA4's enhanced measurement can send page views on browser history changes. Add a manual page_view in the router or a GTM history trigger on top, and every route is counted twice.

No agreed dataLayer

Without a spec, developers push events with different names, missing values or the ecommerce object in the wrong shape. Tags break quietly the next time a component is refactored.

Events fire on intent, not success

Tracking a submit button or a 'pay' click counts failed validations, declined cards and double clicks. The conversion should fire when the API call succeeds.

Hydration and consent timing

In frameworks like Next.js, scripts can load before consent is known or run again after hydration. Tags fire before consent, or GTM loads twice.

What I set up

My Custom website tracking setup

dataLayer contract

  • Written spec: event names, parameters, types and when each fires
  • GA4 ecommerce format with the ecommerce object cleared between pushes
  • Event IDs generated once and reused for server events
  • Example code for your framework's router and forms
  • A test checklist developers can run before release

GTM & page views

  • GTM loaded once in the app shell or layout
  • Route-change page views from a history trigger or a custom page_view push
  • Enhanced measurement settings adjusted to avoid duplicates
  • Hostname rules for local, preview and staging environments
  • Consent Mode v2 defaults set before GTM loads

Server-side events

  • Server-side GTM on Stape or Google Cloud with a tracking subdomain
  • Backend events for sign-ups and payments, including payment webhooks
  • Meta Conversions API with hashed customer data, fbp and fbc
  • GA4 server events and Google Ads enhanced conversions
  • TikTok Events API if you advertise there

QA

  • Route-by-route page view check in GA4 DebugView
  • Ecommerce and lead events tested in staging and production
  • Browser and server deduplication checked in Meta Events Manager
  • Notes for adding new events as the site changes
Event map

Events I track on Custom website

EventWhen it firesSent to
page_viewFirst load and every client-side route change (once each)GA4, Meta Pixel (PageView)
view_item / ViewContentProduct or plan detail view renderedGA4, Meta Pixel + CAPI
add_to_cart / AddToCartCart API call succeedsGA4, Meta Pixel + CAPI, TikTok
begin_checkout / InitiateCheckoutCheckout view opensGA4, Meta Pixel + CAPI
purchase / PurchaseOrder confirmed by your backend or payment webhookGA4, Google Ads, Meta Pixel + CAPI, TikTok
generate_lead / LeadForm API returns successGA4, Google Ads, Meta Pixel + CAPI
sign_up / CompleteRegistrationAccount created by the backendGA4, Google Ads, Meta Pixel + CAPI
Process

How the setup works

  1. 01

    Audit

    I check how GTM and any pixels are loaded, how routing works, and what GA4 and the ad platforms currently receive on each route.

  2. 02

    Write the spec

    I write the dataLayer contract for your conversions and the page view approach for your router, and walk your developers through it.

  3. 03

    Build

    Your team adds the pushes and backend events; I build the web and server GTM containers that read them.

  4. 04

    Test

    I test every route and conversion on staging, then production, and check that nothing is counted twice.

  5. 05

    Hand over

    You get the spec, container notes and a short checklist to run whenever the front-end changes.

Page views in single-page apps

There are two clean ways to track page views in an SPA. One is to let GTM listen for browser history changes and send a page_view on each. The other is to push a page_view event from your router after each navigation, with the page path and title, and fire GA4 from that. The second is more reliable when titles or content update after the URL changes.

Whichever you use, it should be the only source. GA4's enhanced measurement has its own history-based page view setting, and leaving it on alongside a manual setup is the most common reason SPAs report double page views.

Why a dataLayer contract matters

On a custom site, tracking breaks when the code changes and nobody knows a tag depended on it. A dataLayer contract fixes that. It lists each event, its parameters and exactly when it should be pushed, such as after the order API returns success. Developers build to it, GTM reads only from it, and nobody scrapes values from the page.

The same contract covers server events. Your backend sends payments and sign-ups to server-side GTM with the event ID from the browser push, so Meta and Google receive both copies and keep one.

Hand-coded HTML sites

A static or hand-coded HTML site is simpler: GTM goes in the head and body of every page or the shared template, and forms push an event when the form handler returns success. If the form posts to a third-party service, I check whether a thank-you page or the service's own events are the more reliable signal.

Case studies

Related projects

Soulcraft BD: Meta Pixel & CAPI project by Chamok Mahonto
Meta Pixel & CAPI

Soulcraft BD

Hybrid Meta tracking for a Next.js menswear store: five funnel events sent from browser and server, deduplicated in Meta, and mirrored into GA4 ecommerce through server-side GTM.

  • Meta Pixel
  • Conversions API
  • GA4 Enhanced eCommerce
Read case study
FAQ

Custom website tracking FAQ

How do I track page views in a React or Next.js app?

Send a page_view on each route change, either from a GTM history change trigger or from a dataLayer push in your router. Use one method only, and check GA4's enhanced measurement history setting so routes aren't counted twice.

Should GTM or gtag.js be used on a custom site?

I usually recommend GTM with a dataLayer, because marketing tags can then change without a code release. Developers only maintain the dataLayer pushes in the spec, not individual pixels.

Do I need server-side tracking on a custom website?

For payments and sign-ups it's worth it. Your backend already knows when these happen, so sending them server-side makes Meta CAPI and Google Ads data more complete and less affected by ad blockers.

What does a dataLayer spec include?

Each event's name, its parameters and their formats, when it should fire, and an example push. For ecommerce it follows GA4's recommended structure, and it includes the event ID used for deduplication.

Can you work with my in-house developers?

Yes. I write the spec and build GTM and server-side GTM, your developers implement the pushes, and I test the result before and after release.

Guides

Read next

Want your Custom website tracking fixed?

Get a free audit of your tracking and ad account. Usually replies within 1–2 hours.