"Tag-nya jalan di semua halaman lain — tapi di halaman thank-you, tidak ada yang terpicu." Kalau situs kamu dibangun dengan React, Next.js, Vue, atau framework SPA lainnya, ini adalah salah satu kegagalan conversion tracking yang paling umum. Dan ini tidak terlihat di preview tag management kamu karena preview bekerja dengan cara yang berbeda dari navigasi user sungguhan.
Akar masalahnya sederhana: SPA tidak melakukan page load sungguhan. Browser memuat shell-nya sekali, dan setiap "halaman" berikutnya adalah pertukaran DOM yang digerakkan oleh JavaScript. Tag yang bergantung pada page load — yang mana ini berlaku untuk sebagian besar tag — tidak pernah melihat navigasi itu terjadi.
Panduan ini menjelaskan kenapa tracking SPA rusak, cara memperbaikinya untuk setiap framework utama, dan cara memverifikasi bahwa itu berfungsi di production.
1. Kenapa cara pemicuan tag tradisional tidak berfungsi di SPA
Di situs multi-page tradisional, setiap navigasi halaman memicu full browser page load. Sistem tag management (GTM, gtag hardcoded, Meta Pixel, dll.) terpicu pada setiap event DOMContentLoaded atau window.onload. Tag yang dikonfigurasi untuk terpicu pada "All Pages" benar-benar terpicu di semua halaman.
Di SPA:
| Situs multi-page | SPA |
|---|---|
| Klik link → browser meminta HTML baru → full page load | Klik link → JavaScript menukar DOM → URL berubah lewat History API |
Setiap halaman memicu DOMContentLoaded |
DOMContentLoaded hanya terpicu sekali (saat initial load saja) |
| Trigger "All Pages" di GTM terpicu di setiap navigasi | Trigger "All Pages" di GTM hanya terpicu di load pertama |
Tag konversi di /thank-you terpicu saat halaman dimuat |
Tag konversi di /thank-you tidak pernah terpicu (tidak terjadi page load) |
Ini berarti tag konversi yang diset untuk terpicu "saat halaman thank-you dimuat" akan terpicu nol kali — karena halaman thank-you tidak pernah "dimuat" dalam artian yang dipahami browser. User bernavigasi ke sana, URL berubah, konten muncul, tapi tidak ada event page load yang terjadi.
2. Tiga pendekatan untuk memperbaikinya
Pendekatan A: Push virtual pageview ke data layer
Metode paling andal untuk setup berbasis GTM. Ketika route SPA berubah, push sebuah event ke data layer. GTM memicu trigger pada event tersebut, bukan trigger "Page View" bawaan.
// Setiap kali route berubah, push ke dataLayer
dataLayer.push({
event: 'virtual_pageview',
page_path: '/thank-you',
page_title: 'Thank You'
});
Di GTM:
- Buat trigger Custom Event dengan nama event
virtual_pageview - Setel tag konversi kamu untuk terpicu pada trigger ini, dengan kondisi pada
page_path(misalnya,page_path equals /thank-you)
Pendekatan ini memisahkan keputusan "kapan harus terpicu" dari framework — kode aplikasi kamu yang push event, GTM yang menangani logika tag.
Pendekatan B: Gunakan trigger History Change di GTM
GTM punya trigger bawaan "History Change" yang terpicu setiap kali History API browser digunakan (yaitu cara SPA mengubah URL). Ini bisa berfungsi tanpa perubahan kode apa pun di aplikasi kamu.
Setup:
- Di GTM, buat trigger bertipe History Change
- Tambahkan kondisi berdasarkan
{{Page Path}}(misalnya,Page Path equals /thank-you) - Pasang tag konversi kamu ke trigger ini
Catatan penting: Trigger History Change terpicu pada setiap perubahan URL, termasuk navigasi back/forward. Tanpa kondisi yang cermat, tag konversi kamu bisa terpicu ketika user menavigasi mundur melewati halaman thank-you tanpa benar-benar melakukan konversi lagi. Ini adalah sumber umum duplicate conversion di SPA. Kalau tag GTM kamu sama sekali tidak terpicu, lihat kenapa tag GTM terkadang tidak melacak untuk troubleshooting yang lebih luas.
Pendekatan C: Picu tag langsung di kode aplikasi kamu
Untuk tag hardcoded (yang tidak memakai GTM), panggil fungsi tracking langsung di komponen atau route handler kamu.
// React: terpicu di useEffect saat komponen mount
useEffect(() => {
gtag('event', 'conversion', {
'send_to': 'AW-XXXXXXX/YYYYYYY',
'value': 100,
'currency': 'USD'
});
}, []);
// Next.js App Router: terpicu di client component pada halaman thank-you
'use client'
import { useEffect } from 'react'
export default function ThankYou() {
useEffect(() => {
gtag('event', 'conversion', {
'send_to': 'AW-XXXXXXX/YYYYYYY',
'value': 100,
'currency': 'USD'
});
}, []);
return <h1>Terima kasih atas pembelian Anda</h1>
}
Kelebihan: Tidak ada kerumitan GTM; konversi terpicu tepat kapan dan di mana kamu mau.
Risiko: Mencampur kode tracking ke dalam kode aplikasi bisa membuatnya lebih sulit dikelola. Kalau kamu punya banyak tag di banyak halaman, GTM biasanya lebih mudah dirawat.
Dukungan SPA bawaan GA4 (Enhanced Measurement)
Sebelum membangun tracking SPA custom, cek dulu apakah fitur bawaan GA4 sudah mencakup kebutuhan kamu. GA4 punya opsi "Page changes based on browser history events" di bawah Enhanced Measurement:
Cara mengaktifkan: Admin → Data Streams → Web → Enhanced Measurement → Page Views → Show advanced settings → Aktifkan "Page changes based on browser history events."
Setelah diaktifkan, GA4 secara otomatis memicu event page_view setiap kali History API browser memperbarui URL — persis yang terjadi di SPA. Ini berfungsi tanpa perubahan kode atau konfigurasi GTM apa pun.
Peringatan penting: jangan sampai double-count. Kalau kamu menggunakan GTM untuk mengirim event page_view pada perubahan route DAN pengaturan GA4 ini juga aktif, setiap navigasi SPA akan terhitung dua kali. Pilih salah satu: baik Enhanced Measurement GA4 yang menangani pageview secara otomatis, atau setup GTM kamu — jangan keduanya. Ini adalah salah satu kesalahan konfigurasi SPA tracking yang paling umum.
Keterbatasan: Fitur ini menangani pageview tracking, bukan conversion event tracking. Kamu tetap perlu memicu event khusus konversi (purchase, sign_up, dll.) lewat GTM atau kode aplikasi pada momen yang tepat.
3. Hal-hal yang perlu diwaspadai per framework
React (Create React App, Vite)
- Masalahnya: React Router mengubah URL lewat History API. Tidak ada page load yang terpicu.
- Perbaikannya: Gunakan
useEffectdi komponen tujuan, atau dengarkan perubahan route di router kamu dan push ke data layer. - Yang perlu diwaspadai:
useEffectberjalan setelah render, artinya konversi terpicu sedikit setelah DOM diperbarui. Ini biasanya tidak masalah, tapi kalau user berpindah sangat cepat (misalnya, redirect instan), tag mungkin tidak sempat selesai.
Next.js
- App Router: Halaman bisa di-render di server (RSC) atau di client. Tag konversi harus berjalan di client. Gunakan komponen
'use client'denganuseEffect, atau gunakan komponennext/scriptdengan strategiafterInteractive. - Pages Router: Gunakan API
router.eventsuntuk mendengarkanrouteChangeCompletedan push ke data layer. - Static export vs. server: Kalau kamu memakai
output: 'export', perilakunya mirip SPA standar. Kalau kamu memakai server-side rendering, page load awal memicu tag secara normal — hanya navigasi sisi client yang terdampak. - Yang perlu diwaspadai: Next.js melakukan prefetch link. Prefetching tidak memicu perubahan route, tapi kalau tag kamu terpicu pada event yang salah, aktivitas DOM terkait prefetch bisa menyebabkan pemicuan hantu (phantom fire).
Vue (Nuxt)
- Vue Router: Dengarkan
router.afterEach()untuk push virtual pageview ke data layer. - Nuxt: Gunakan hook
page:finishatau plugin yang terpicu pada perubahan route. - Yang perlu diwaspadai: Sistem transisi Vue bisa menunda pembaruan DOM. Kalau tag kamu memeriksa keberadaan elemen DOM di halaman tujuan, elemen itu mungkin belum ada saat event perubahan route terpicu.
Angular
- Angular Router: Subscribe ke
Router.eventsdan filter untuk eventNavigationEnd. Push ke data layer pada setiapNavigationEnd. - Yang perlu diwaspadai: Zone.js milik Angular bisa mengganggu script pihak ketiga. Kalau sebuah tag dimuat secara async dan berjalan di luar zone Angular, perilakunya bisa jadi tidak terduga.
4. Jebakan duplicate conversion
SPA membuat duplicate conversion lebih mungkin terjadi dibanding situs multi-page, karena dua alasan:
-
Navigasi history: User menyelesaikan pembelian, mendarat di
/thank-you, lalu menekan tombol back lalu forward — trigger History Change terpicu lagi, dan konversi terhitung dua kali. -
Component re-mounting: Di React, kalau komponen thank-you unmount lalu mount lagi (karena re-render dari parent atau route yang flicker),
useEffectberjalan lagi dan memicu tag untuk kedua kalinya.
Cara mengatasi:
- Gunakan flag sekali pakai: set sebuah variabel (session storage, React ref, atau variabel data layer) setelah pemicuan pertama, dan cek variabel itu sebelum memicu lagi
- Di GTM, gunakan opsi "trigger fires once per page" — tapi perhatikan bahwa di SPA, "per page" berarti per initial page load, bukan per perubahan route
- Deduplikasi di sisi platform: gunakan parameter
transaction_id(Google Ads) ataueventID(Meta) agar platform bisa mengabaikan sinyal duplikat
5. Kebersihan data layer antar navigasi SPA
Di situs multi-page, setiap navigasi mereset data layer — ia mulai dari awal di setiap page load. Di SPA, data layer bertahan lintas perubahan route. Ini menciptakan masalah yang halus tapi merusak: nilai basi dari halaman sebelumnya mencemari event di halaman saat ini.
Bagaimana nilai basi menyebabkan data konversi yang salah
Seorang user melihat Produk A ($199), menambahkannya ke keranjang, lalu berpindah ke Produk B ($49) dan membeli. Kalau data layer masih menyimpan nilai Produk A saat event purchase terpicu, konversi bisa jadi melaporkan $199, bukan $49 — atau lebih buruk lagi, menyertakan data item Produk A bersama Produk B.
Solusinya: flush sebelum setiap virtual pageview
Reset variabel data layer yang relevan pada setiap perubahan route, sebelum push pageview baru:
// Saat route berubah — flush, lalu push
dataLayer.push({
event: 'spa_cleanup',
ecommerce: null // membersihkan data ecommerce GA4
});
dataLayer.push({
event: 'spa_pageview',
spa_page_path: newPath,
spa_page_title: newTitle
});
Menyetel ecommerce: null secara khusus direkomendasikan oleh Google untuk membersihkan data ecommerce basi antar event (lihat panduan kami tentang event purchase ecommerce GA4 untuk struktur data layer lengkapnya). Untuk variabel non-ecommerce, secara eksplisit setel ke undefined atau push objek baru yang menimpa nilai sebelumnya.
Yang perlu diwaspadai
- Transaction ID dari pembelian sebelumnya yang tetap bertahan — menyebabkan event berikutnya membawa transaction ID lama, yang bisa mendeduplikasi konversi baru yang sebenarnya asli
- Ketidakcocokan mata uang — user yang melihat produk USD lalu beralih ke produk JPY; kalau currency tidak di-push ulang, nilai lama akan tetap bertahan
- Versi GTM Data Layer Variable — variabel Version 1 bertahan sampai dibersihkan secara eksplisit; variabel Version 2 bersifat event-scoped tapi tetap bisa bocor kalau dibaca pada timing yang salah
6. Cara memverifikasi conversion tracking SPA
Langkah 1: Periksa dengan tab Network di DevTools
Navigasikan ke halaman konversi di dalam SPA (jangan reload — bernavigasilah dari halaman lain). Di tab Network, filter untuk request konversi:
- Google Ads:
googleadservices.com/pagead/conversion/ - GA4:
google-analytics.com/g/collect - Meta:
facebook.com/tr
Kalau request muncul setelah navigasi, tag terpicu dengan benar. Kalau hanya muncul saat full page reload, tag di-setup hanya untuk page load dan tidak menangkap navigasi SPA.
Langkah 2: Uji skenario tombol back
Setelah memicu konversi, tekan tombol back, lalu tombol forward. Periksa lagi tab Network — apakah request konversi terpicu untuk kedua kalinya? Kalau ya, kamu punya masalah duplicate conversion.
Langkah 3: Keterbatasan mode Preview GTM
Mode Preview GTM bisa tidak dapat diandalkan untuk pengujian SPA. Panel Preview terkadang menampilkan tag sebagai "fired" padahal request network sesungguhnya tidak terkirim, atau mungkin tidak merefleksikan event History Change dengan benar. Selalu lakukan cross-check dengan tab Network, jangan cuma mengandalkan GTM Preview.
Langkah 4: Uji di lingkungan yang bersih
SPA bisa berperilaku berbeda tergantung pada:
- Apakah halaman diakses lewat entri URL langsung (full page load) vs. navigasi internal (perubahan route SPA)
- Apakah service worker melakukan caching pada halaman tersebut
- Apakah user memakai ad blocker yang mengganggu tag
Uji kedua jalur: load langsung via URL ke halaman thank-you, dan navigasi ke halaman itu dari dalam aplikasi.
7. Server-side tagging untuk SPA
SPA sangat rentan terhadap kegagalan client-side tracking: ad blocker mencegat request ke googleadservices.com, facebook.com/tr, dan endpoint tracking lainnya; consent management platform mungkin memblokir tag sebelum terpicu; dan JavaScript client-side yang berat bisa menunda atau mencegah eksekusi tag.
Server-side Google Tag Manager (sGTM) mengatasi masalah ini dengan merutekan request tracking lewat domain kamu sendiri:
| Masalah | Client-side saja | Dengan server-side GTM |
|---|---|---|
| Ad blocker memblokir request konversi | Request diblokir, konversi hilang | Request dikirim ke domain kamu dulu, lalu diteruskan di sisi server — tidak diblokir |
| Consent Mode v2 di SPA | Status consent harus dicek ulang di setiap virtual pageview (tidak ada page reload untuk inisialisasi ulang) | Container server-side bisa menegakkan status consent secara konsisten |
| Kecepatan eksekusi tag | Banyak script pihak ketiga dimuat di client, memperlambat SPA | Hanya satu request ke server kamu; server yang menangani distribusi ke banyak platform |
Kapan mempertimbangkan server-side tagging untuk SPA kamu:
- Tingkat penggunaan ad blocker di audiens kamu melebihi 20–30%
- Kamu melihat kesenjangan besar antara jumlah order dari server dan konversi yang dilaporkan
- Kamu perlu mematuhi regulasi privasi UE dan ingin kontrol yang lebih ketat atas data apa yang keluar dari browser
Catatan praktis: Server-side GTM tidak menggantikan client-side SPA tracking — ia melengkapinya. Kamu tetap perlu memicu event yang tepat pada waktu yang tepat di kode SPA kamu; server-side tagging mengubah di mana event tersebut diproses, bukan kapan event itu terpicu.
8. Konfigurasi GTM untuk SPA: contoh yang berfungsi
Berikut setup GTM lengkap untuk melacak konversi di SPA:
Push data layer (di kode aplikasi kamu):
// Jalankan ini di setiap perubahan route
function onRouteChange(path, title) {
window.dataLayer = window.dataLayer || [];
dataLayer.push({
event: 'spa_pageview',
spa_page_path: path,
spa_page_title: title
});
}
Trigger GTM:
- Tipe: Custom Event
- Nama event:
spa_pageview - Trigger ini terpicu pada: Some Custom Events
- Kondisi:
spa_page_path equals /thank-you
Tag GTM:
- Tipe: Google Ads Conversion Tracking (atau GA4 Event, Meta Pixel, dll.)
- Trigger: trigger konversi SPA di atas
Ini menjaga logika tracking tetap di GTM (mudah dikelola) sambil mengandalkan aplikasi kamu untuk memberi sinyal kapan navigasi terjadi (andal di SPA).
Pertanyaan yang sering diajukan
T. Apakah saya perlu mengubah setup GTM saya kalau bermigrasi dari situs multi-page ke SPA? J. Ya. Tag apa pun yang terpicu pada trigger bawaan "All Pages" atau "Page View" akan berhenti terpicu pada navigasi SPA. Kamu perlu menambahkan trigger History Change atau trigger event data layer untuk setiap tag yang seharusnya terpicu pada perubahan route.
T. Konversi pengiriman form saya tidak terlacak di SPA saya. Apa bedanya? J. Di SPA, pengiriman form sering terjadi lewat JavaScript (AJAX/fetch) tanpa navigasi halaman sama sekali, jadi baik trigger page load maupun trigger History Change tidak akan menangkapnya. Kamu perlu memicu event konversi langsung di success handler form, atau push event data layer saat pengiriman selesai. Untuk lebih lanjut tentang pola ini, lihat kenapa pengiriman form tidak terlacak.
T. Tag konversi saya terpicu pada full page reload tapi tidak pada navigasi SPA. Ada perbaikan cepat? J. Perbaikan tercepat adalah trigger History Change di GTM — tidak perlu perubahan kode. Tapi demi keandalan, push custom event ke data layer dari kode aplikasi kamu lebih baik, karena kamu mengontrol persis kapan event itu terpicu.
T. Apakah gtag.js dari Google menangani navigasi SPA secara otomatis?
J. Google tag (gtag.js) memang punya sejumlah dukungan SPA — ia bisa mendeteksi perubahan History API dan mengirim event pageview. Tapi event konversi (seperti gtag('event', 'conversion', ...)) tetap perlu dipanggil secara eksplisit pada momen yang tepat. Jangan mengandalkan deteksi otomatis untuk conversion tracking.
T. Kami memakai server-side rendering (SSR). Apakah ini tetap berlaku?
J. Sebagian berlaku. Page load awal adalah page load sungguhan (SSR mengirim HTML lengkap), jadi tag terpicu normal pada hit pertama. Tapi navigasi berikutnya di dalam aplikasi tetap merupakan navigasi SPA sisi client — itu tetap butuh penanganan SPA. Di Next.js, ini berarti page load pertama berfungsi baik-baik saja, tapi mengklik <Link> ke halaman lain adalah transisi SPA.
Kesimpulan: tag-nya tidak rusak — ia hanya tidak pernah melihat page load
Kegagalan conversion tracking SPA bukan disebabkan oleh tag yang rusak. Tag-nya baik-baik saja; ia hanya sedang menunggu sebuah event (page load) yang tidak pernah terjadi lagi setelah load awal. Perbaikannya selalu mengikuti pola yang sama: beri tahu tag kapan sebuah navigasi terjadi, entah lewat push data layer, listener History Change, atau pemanggilan fungsi langsung di kode komponen kamu.
Masalah yang lebih halus adalah verifikasi. Gunakan checklist verifikasi conversion tracking kami sebagai titik awal, tapi tambahkan pemeriksaan khusus SPA. Karena SPA berperilaku berbeda tergantung bagaimana user tiba di halaman tersebut, sebuah konversi bisa berfungsi di satu skenario dan gagal di skenario lain — dan GTM Preview tidak selalu menangkap perbedaan itu. Selalu verifikasi dengan tab Network, dan selalu uji baik akses URL langsung maupun navigasi di dalam aplikasi.
ConversionOK menguji halaman live kamu di browser independen dan memeriksa apakah sinyal konversi benar-benar terkirim, apa pun cara halaman itu dibangun. Untuk SPA, ini menangkap kasus di mana tag sudah terpasang tapi tidak pernah terpicu di halaman konversi — masalah yang tidak terlihat sampai kamu memeriksanya di level network.