Przejdź do treści

Wdrożenie Google Tag Manager w Next.js App Router

Jak wdrożyć Google Tag Manager w Next.js bez chaosu w dataLayer. Zobacz jak obsługiwać zdarzenia e commerce tryb consent mode oraz zaawansowane debugowanie.

Maciej Sala

Founder StriveLab

8 min czytaniaOpublikowano 25 września 2025 (Aktualizacja 25 maja 2026)

Dlatego dobre wdrożenie w Next.js nie polega na wklejeniu snippetu i nadziei, że „Google sobie poradzi”. Polega na zbudowaniu jasnego kontraktu między aplikacją a kontenerem: deweloper wysyła spójne eventy do , a marketer buduje na nich tagi, triggery i konwersje bez grzebania w kodzie.

Kiedy Google Tag Manager ma sens w Next.js?

Jeżeli strona używa wyłącznie GA4 i ma trzy podstawowe eventy, GTM nie zawsze jest najlepszym wyborem. Bezpośredni Google tag albo @next/third-parties/google może być prostszy: mniej warstw, mniej miejsc do pomyłki, łatwiejszy code review.

GTM zaczyna wygrywać, gdy dochodzą kolejne potrzeby:

  • Google Ads conversion tracking i remarketing,
  • Meta Pixel, LinkedIn Insight Tag, TikTok Pixel lub inne platformy reklamowe,
  • testy A/B, narzędzia heatmapowe i skrypty marketing automation,
  • wiele eventów konwersji zarządzanych przez marketing,
  • potrzeba szybkiej zmiany tagów bez deploya aplikacji.

Wtedy GTM jest warstwą operacyjną dla marketingu, a dataLayer staje się między produktem a analityką. To API musi być stabilne, nazwane i udokumentowane. Bez tego marketer będzie budował triggery na klasach CSS, tekście przycisku albo strukturze DOM, która zmieni się przy następnym refactorze.

Instalacja GTM w Next.js App Router

Next.js udostępnia komponent GoogleTagManager w @next/third-parties/google. Oficjalnie można go dodać w root layout i przekazać gtmId. To dobry punkt startowy, szczególnie gdy chcesz użyć wspieranego przez Next.js sposobu ładowania third-party scripts.

app/layout.tsx
Code
import { GoogleTagManager } from '@next/third-parties/google'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <body>{children}</body>
      <GoogleTagManager gtmId="GTM-XXXXXXX" />
    </html>
  )
}

W bardziej kontrolowanych wdrożeniach nadal często wybieram własny komponent oparty o next/script. Powód jest praktyczny: łatwiej wymusić kolejność consent defaults, warunki ładowania, środowiska i fallback noscript.

components/GTM.tsx
Code
'use client'
 
import Script from 'next/script'
 
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID
 
export function GTMHead() {
  if (!GTM_ID || process.env.NODE_ENV !== 'production') return null
 
  return (
    <Script id="gtm-script" strategy="afterInteractive">
      {`
        (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_ID}');
      `}
    </Script>
  )
}
 
export function GTMNoScript() {
  if (!GTM_ID || process.env.NODE_ENV !== 'production') return null
 
  return (
    <noscript>
      <iframe
        src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
        height="0"
        width="0"
        style={{ display: 'none', visibility: 'hidden' }}
      />
    </noscript>
  )
}
app/layout.tsx
Code
import { GTMHead, GTMNoScript } from '@/components/GTM'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <body>
        <GTMNoScript />
        {children}
        <GTMHead />
      </body>
    </html>
  )
}

Nie ładuj samego kontenera GTM przez beforeInteractive, jeśli nie masz bardzo konkretnego powodu. Consent defaults to inna sprawa: one faktycznie powinny pojawić się przed tagami, które zależą od zgody. Sam kontener zwykle wystarczy uruchomić po interaktywności strony.

DataLayer jako kontrakt między deweloperem a marketingiem

dataLayer to tablica obiektów JavaScript. Google Tag Manager czyta kolejne wpisy, przetwarza je po kolei i odpala tagi, których triggery pasują do danego eventu. Najważniejszy detal: jeśli pushujesz dane bez event, GTM może mieć problem z przewidywalnym odpaleniem tagów we właściwym momencie. Dlatego event powinien być jawny.

Zacznij od małego wrappera. Nie pushuj obiektów bezpośrednio z każdego komponentu, bo po kilku miesiącach nikt nie będzie wiedział, kto wysyła form_submit, kto formSubmit, a kto lead_sent.

lib/dataLayer.ts
Code
type DataLayerEvent = Record<string, unknown> & {
  event: string
}
 
declare global {
  interface Window {
    dataLayer?: Array<Record<string, unknown>>
    gtag?: (...args: unknown[]) => void
  }
}
 
export function pushToDataLayer(data: DataLayerEvent) {
  if (typeof window === 'undefined') return
 
  window.dataLayer = window.dataLayer || []
  window.dataLayer.push(data)
}
 
export function pushEcommerceEvent(
  event: string,
  ecommerce: Record<string, unknown>,
) {
  if (typeof window === 'undefined') return
 
  window.dataLayer = window.dataLayer || []
  window.dataLayer.push({ ecommerce: null })
  window.dataLayer.push({ event, ecommerce })
}

W dużym projekcie ten plik szybko warto rozbić na moduły: analytics/pageView, analytics/ecommerce, analytics/forms, analytics/consent. Ważne, żeby komponent nie znał szczegółów struktury ecommerce ani nazw parametrów używanych przez GA4.

Page view w App Routerze Next.js

Najczęstszy błąd w Next.js polega na tym, że kontener GTM ładuje się raz, a zespół zakłada, że page views będą zliczane jak w klasycznej stronie. Nie będą. App Router zmienia widok bez pełnego przeładowania dokumentu, więc potrzebujesz własnego eventu na zmianę pathname i searchParams.

components/GTMPageView.tsx
Code
'use client'
 
import { Suspense, useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import { pushToDataLayer } from '@/lib/dataLayer'
 
function GTMPageViewInner() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
 
  useEffect(() => {
    const query = searchParams?.toString()
    const pagePath = query ? `${pathname}?${query}` : pathname
 
    pushToDataLayer({
      event: 'page_view',
      page_path: pagePath,
      page_location: window.location.href,
      page_title: document.title,
    })
  }, [pathname, searchParams])
 
  return null
}
 
export function GTMPageView() {
  return (
    <Suspense fallback={null}>
      <GTMPageViewInner />
    </Suspense>
  )
}

W GTM tworzysz potem Custom Event Trigger dla page_view i podpinasz pod niego tag eventowy . Jeśli używasz Google tag jako konfiguracji bazowej, wyłącz automatyczne wysyłanie page view tam, gdzie grozi duplikat. Jedno źródło prawdy jest ważniejsze niż „więcej danych”.

Ecommerce w GTM: mniej kreatywności, więcej standardu GA4

Przy ecommerce nie wymyślaj własnych nazw, jeśli nie musisz. GA4 ma rekomendowane eventy i strukturę items[], więc używaj view_item, add_to_cart, begin_checkout, purchase oraz standardowych parametrów. Dzięki temu raporty e-commerce, import konwersji do Google Ads i diagnostyka będą dużo prostsze.

lib/dataLayer-ecommerce.ts
Code
import { pushEcommerceEvent } from './dataLayer'
 
type Product = {
  id: string
  name: string
  brand?: string
  category?: string
  price: number
}
 
type CartItem = {
  product: Product
  quantity: number
}
 
type Order = {
  id: string
  totalValue: number
  tax?: number
  shippingCost?: number
  couponCode?: string
  items: CartItem[]
}
 
function toItem(product: Product, index?: number) {
  return {
    item_id: product.id,
    item_name: product.name,
    item_brand: product.brand,
    item_category: product.category,
    price: product.price / 100,
    quantity: 1,
    index,
  }
}
 
export const dlEcommerce = {
  viewItemList: (listName: string, products: Product[]) =>
    pushEcommerceEvent('view_item_list', {
      item_list_id: listName,
      item_list_name: listName,
      items: products.map((product, index) => toItem(product, index)),
    }),
 
  viewItem: (product: Product) =>
    pushEcommerceEvent('view_item', {
      currency: 'PLN',
      value: product.price / 100,
      items: [toItem(product)],
    }),
 
  addToCart: (product: Product, quantity = 1) =>
    pushEcommerceEvent('add_to_cart', {
      currency: 'PLN',
      value: (product.price * quantity) / 100,
      items: [{ ...toItem(product), quantity }],
    }),
 
  beginCheckout: (items: CartItem[], total: number) =>
    pushEcommerceEvent('begin_checkout', {
      currency: 'PLN',
      value: total / 100,
      items: items.map((item) => ({
        ...toItem(item.product),
        quantity: item.quantity,
      })),
    }),
 
  purchase: (order: Order) =>
    pushEcommerceEvent('purchase', {
      transaction_id: order.id,
      currency: 'PLN',
      value: order.totalValue / 100,
      tax: (order.tax ?? 0) / 100,
      shipping: (order.shippingCost ?? 0) / 100,
      coupon: order.couponCode,
      items: order.items.map((item) => ({
        ...toItem(item.product),
        quantity: item.quantity,
      })),
    }),
}

Najważniejsze są dwie zasady. Po pierwsze: wartości pieniężne powinny być liczbami w walucie raportowania, nie stringami typu "149,99 zł". Po drugie: purchase musi mieć stabilny transaction_id, bo bez niego trudno diagnozować duplikaty i import konwersji do Google Ads.

Custom events w GTM dla marketingu

Nie każdy event jest ecommerce. W stronach usługowych, SaaS-ach i aplikacjach B2B ważniejsze będą formularze, kliknięcia CTA, przejścia między krokami onboardingowymi, pobrania plików i interakcje z kalkulatorem.

lib/dataLayer-custom.ts
Code
import { pushToDataLayer } from './dataLayer'
 
export const dlCustom = {
  ctaClick: (ctaName: string, location: string) =>
    pushToDataLayer({
      event: 'cta_click',
      cta_name: ctaName,
      cta_location: location,
    }),
 
  formStart: (formName: string) =>
    pushToDataLayer({
      event: 'form_start',
      form_name: formName,
    }),
 
  formSubmit: (formName: string, success: boolean) =>
    pushToDataLayer({
      event: 'form_submit',
      form_name: formName,
      form_success: success,
    }),
 
  fileDownload: (fileName: string, fileUrl: string) =>
    pushToDataLayer({
      event: 'file_download',
      file_name: fileName,
      file_url: fileUrl,
    }),
 
  signUp: (method: string) =>
    pushToDataLayer({
      event: 'sign_up',
      signup_method: method,
    }),
}

Nazwy eventów powinny być stabilne i nudne. cta_click jest lepsze niż heroBigGreenButtonClicked, bo może obsłużyć wiele miejsc w produkcie, a szczegóły przenosisz do parametrów. To samo dotyczy formularzy: jeden form_submit z form_name i form_success daje lepszy model danych niż piętnaście osobnych eventów.

Dokumentacja dataLayer dla marketera

Dobry dataLayer bez dokumentacji nadal jest słaby. Marketer musi wiedzieć, jakie eventy istnieją, kiedy się odpalają i jakie parametry może mapować w GTM. Najprostszy format to tabela w Notion, Google Docs albo markdown w repo.

docs/data-layer-contract.md
Code
## page_view
 
Kiedy: każda zmiana route w App Router oraz hard reload.
Parametry: page_path, page_title, page_location.
Typowy trigger w GTM: Custom Event = page_view.
 
## cta_click
 
Kiedy: kliknięcie przycisku CTA mierzonego marketingowo.
Parametry: cta_name, cta_location.
Typowy trigger w GTM: Custom Event = cta_click.
 
## form_submit
 
Kiedy: próba wysłania formularza.
Parametry: form_name, form_success.
Typowy trigger w GTM: Custom Event = form_submit + warunek form_success = true.
 
## purchase
 
Kiedy: potwierdzone zamówienie po stronie aplikacji.
Parametry: ecommerce.transaction_id, ecommerce.value, ecommerce.currency, ecommerce.items[].
Typowy trigger w GTM: Custom Event = purchase.

Taki dokument zmienia rozmowę. Zamiast „czy możesz dodać event na ten przycisk?” pojawia się pytanie „czy ten przycisk powinien użyć istniejącego cta_click, czy potrzebujemy nowego typu zdarzenia?”. To jest różnica między analityką utrzymywalną a przypadkowym zbiorem tagów.

Konfiguracja tagów w Google Tag Manager

Po stronie GTM marketer zwykle tworzy kilka warstw.

Google tag / GA4 config inicjalizuje pomiar. Jeśli page view wysyłasz ręcznie, wyłącz automatyczny page view tam, gdzie powodowałby duplikaty. Ten tag nie powinien być odpowiedzialny za mierzenie każdej nawigacji w App Routerze.

GA4 Event Tag: page_view powinien odpalać się na Custom Event page_view i mapować parametry page_path, page_title, page_location.

GA4 Event Tag: form_submit może odpalać się na Custom Event form_submit, ale często warto dodać warunek form_success equals true, żeby nie raportować walidacyjnych porażek jako leadów.

Google Ads Conversion Tag zwykle podpinasz pod event biznesowy, np. purchase albo generate_lead. Wartość konwersji mapujesz z Data Layer Variable, a transaction_id przekazujesz tam, gdzie narzędzie to obsługuje.

Remarketing i piksele zewnętrzne powinny mieć jasno opisane warunki odpalenia i consent requirements. To nie są „niewinne skrypty”; wpływają na prywatność, performance i jakość danych.

Consent to nie jest popup z przyciskiem „Akceptuję”. Dla tagów ważne jest to, jaki stan zgody obowiązuje w momencie odpalenia eventu. Dlatego defaulty powinny być ustawione zanim tagi zależne od consent zaczną działać.

Jeśli obsługujesz banner w aplikacji, możesz ustawić domyślne zgody przed kontenerem GTM:

components/ConsentDefaults.tsx
Code
import Script from 'next/script'
 
export function ConsentDefaults() {
  return (
    <Script id="consent-defaults" strategy="beforeInteractive">
      {`
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('consent', 'default', {
          analytics_storage: 'denied',
          ad_storage: 'denied',
          ad_user_data: 'denied',
          ad_personalization: 'denied',
          wait_for_update: 500
        });
      `}
    </Script>
  )
}

Po decyzji użytkownika wysyłasz update:

lib/consent.ts
Code
import { pushToDataLayer } from './dataLayer'
 
export function grantMarketingConsent() {
  window.gtag?.('consent', 'update', {
    analytics_storage: 'granted',
    ad_storage: 'granted',
    ad_user_data: 'granted',
    ad_personalization: 'granted',
  })
 
  pushToDataLayer({
    event: 'consent_update',
    consent_analytics: 'granted',
    consent_ads: 'granted',
  })
}

Jeśli consent jest zarządzany przez CMP w GTM, nie doklejaj własnych Custom HTML tagów z przypadkowym gtag('consent'). Oficjalne zalecenie Google jest proste: do consent w Tag Managerze używaj Consent APIs i odpowiednich szablonów, bo stan zgody musi być przetworzony przed eventami, które zależą od tej zgody.

Debugowanie GTM w Next.js

Najlepsze debugowanie zaczyna się od GTM Preview. Uruchamiasz Preview, wpisujesz URL, przechodzisz przez scenariusz i sprawdzasz:

  • czy event trafił do dataLayer,
  • czy trigger pasuje do eventu,
  • czy tag się odpalił,
  • jakie wartości miały Data Layer Variables,
  • czy consent pozwalał na odpalenie taga.

Drugim narzędziem jest Tag Assistant. Szczególnie przy consent mode pokazuje, czy zgody są ustawiane w poprawnej kolejności i czy tagi nie odpalają się zbyt wcześnie.

Do szybkiej diagnostyki w konsoli przydają się proste komendy:

Code
// Wszystkie dotychczasowe wpisy
console.table(window.dataLayer)
 
// Tylko konkretne eventy
window.dataLayer.filter((entry) => entry.event === 'purchase')
 
// Tymczasowy podgląd nowych pushy
const originalPush = window.dataLayer.push.bind(window.dataLayer)
 
window.dataLayer.push = function (...args) {
  console.log('dataLayer push:', args)
  return originalPush(...args)
}

W development uważaj na React Strict Mode. Jeśli widzisz podwójne eventy lokalnie, najpierw sprawdź produkcyjny build albo preview deployment. Dopiero jeśli duplikaty występują poza dev mode, szukaj błędu w efektach Reacta, automatycznym page view w GA4 albo podwójnie opublikowanych tagach w GTM.

Najczęstsze błędy w Google Tag Manager

Duplikaty page_view zwykle biorą się z jednoczesnego automatycznego page view w Google tagu i ręcznego eventu w App Routerze. Wybierz jeden model i trzymaj się go konsekwentnie.

Brak ecommerce: null powoduje mieszanie danych ecommerce między eventami. To klasyczny problem przy view_item, add_to_cart i purchase.

Triggery oparte na CSS selectorach są kruche. Jeśli marketer podpina konwersję pod .btn-primary:nth-child(2), to refactor UI może zepsuć pomiar bez żadnego błędu w buildzie.

Custom HTML bez review to ryzyko wydajności, bezpieczeństwa i błędów w . Wbudowane szablony tagów są bezpieczniejsze niż dowolny skrypt wklejony do kontenera.

Brak procesu publikacji kończy się tym, że nikt nie wie, która wersja GTM zmieniła dane. Korzystaj z workspace, opisuj zmiany i testuj Preview przed publikacją.

Współpraca deweloper i marketer przy GTM

Najzdrowszy model jest prosty:

  1. Deweloper tworzy i utrzymuje dataLayer contract.
  2. Marketer zgłasza potrzeby pomiarowe językiem biznesowym: lead, zakup, kliknięcie CTA, pobranie pliku.
  3. Deweloper mapuje te potrzeby na eventy i parametry.
  4. Marketer konfiguruje tagi w GTM.
  5. Obie strony sprawdzają scenariusz w Preview mode przed publikacją.

Ten proces może wyglądać wolniej niż „wrzućmy tag od razu”, ale w praktyce oszczędza czas. Mniej duplikatów, mniej niejasnych konwersji, mniej sytuacji, w których kampania optymalizuje się na błędny event.

Kampanie, landing page, tracking konwersji, GA4 i GTM w jednym procesie.
Google Ads i Analityka

Często zadawane pytania

Czy GTM ma sens, jeśli używam tylko GA4?

Niekoniecznie. Jeśli potrzebujesz tylko jednego taga Google i kilku prostych eventów, bezpośrednie wdrożenie GA4 bywa prostsze, czytelniejsze i łatwiejsze do utrzymania. GTM zaczyna mieć sens wtedy, gdy dochodzą Google Ads, remarketing, piksele platform reklamowych, testy A/B albo zespół marketingowy, który musi zmieniać tagi bez każdego deploya.

Czy event page_view w Next.js App Router trzeba wysyłać ręcznie?

W praktyce tak, jeśli chcesz mieć pełną kontrolę nad pomiarem nawigacji w aplikacji SPA. App Router nie przeładowuje całego dokumentu przy przejściu między trasami, dlatego warto wysyłać jawny event page_view po zmianie pathname lub searchParams i dopiero na nim budować trigger w GTM.

Gdzie najlepiej obsłużyć consent mode: w aplikacji czy w GTM?

To zależy od ownershipu. Jeśli banner cookies i preferencje są częścią aplikacji, ustaw default consent przed załadowaniem kontenera GTM i wysyłaj update po decyzji użytkownika. Jeśli zgody są zarządzane przez CMP lub szablon w GTM, trzymaj consent po stronie Tag Managera i korzystaj z Consent APIs. Najgorszy wariant to mieszanie obu podejść bez jasnego kontraktu.

Dlaczego przed eventem ecommerce trzeba pushować ecommerce: null?

W GTM wartości w dataLayer mogą zostawać dostępne dla kolejnych eventów. Jeśli przed nowym eventem ecommerce nie wyczyścisz poprzedniego obiektu, dane z view_item mogą pomieszać się z add_to_cart albo purchase. Dlatego bezpieczny wzorzec to osobny push { ecommerce: null }, a dopiero potem event z aktualnym obiektem ecommerce.

Jak debugować GTM w Next.js, gdy w dev widzę podwójne eventy?

Najpierw sprawdź, czy problem występuje w produkcyjnym buildzie. W development React Strict Mode może uruchamiać efekty dwukrotnie, przez co eventy wyglądają jak zdublowane. Do realnego debugowania używaj GTM Preview, Tag Assistant i produkcyjnego lub preview deploya, a nie tylko lokalnego dev servera.

Czy marketer powinien publikować zmiany w GTM bez review dewelopera?

Przy prostych zmianach opisowych czasem wystarczy proces marketingowy, ale tagi mierzące konwersje, ecommerce, consent, remarketing i Custom HTML powinny przejść review. Najlepszy proces to workspace w GTM, test w Preview mode, weryfikacja dataLayer i dopiero publikacja.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Konfiguracja Google Analytics 4 z paczką next third parties

Integracja Google Analytics 4 w Next.js App Router to coś więcej niż wklejenie snippetu w , ponieważ nawigacja client-side, React Server Components, Consent Mode v2 i wybór między ręcznym gtag.js , a @next/third-parties sprawiają, że łatwo wprowadzić analitykę pozornie poprawną, ale w praktyce generującą zdublowane page views albo niepełne dane - nawiasem mówiąc, obie te sytuacje zdarzyły mi się w przeszłości. Ten artykuł łączy setup GA4 z realnymi niuansami App Router i podstawami planowania zdarzeń.

Maciej Sala

Maciej Sala

Founder StriveLab

Landing page pod Google Ads w Next.js i wysoka konwersja

Jak zbudować landing page w Next.js, która jest szybka, trafna i dobrze domyka ruch z Google Ads bez psucia atrybucji i doświadczenia użytkownika?

Maciej Sala

Maciej Sala

Founder StriveLab

Google Ads Remarketing w React i dynamiczne listy

Jak wdrożyć remarketing Google Ads w aplikacji React? Dynamiczny remarketing ecommerce, listy odbiorców na podstawie zachowań użytkowników i integracja z listami odbiorców w GA4.

Maciej Sala

Maciej Sala

Founder StriveLab