Wstęp#

Ostatnio musiałem zintegrować analitykę w jednym z projektów opartych na Hugo. Zazwyczaj wrzucenie skryptu śledzącego to “pięć minut roboty”, ale jeśli chcemy to zrobić porządnie – czyli wydajnie i z poszanowaniem prywatności użytkowników – sprawa robi się ciekawsza.

Poniżej spisałem moje wnioski i gotową procedurę wdrożenia Google Tag Managera (GTM) oraz Google Analytics 4 (GA4). Skupiłem się na tym, aby kod był czysty, a mechanizm zgód na cookies (Cookie Consent) faktycznie działał, a nie tylko wyglądał.

Dlaczego rozdzielamy GTM i GA4?#

Na początek krótka lekcja architektury, bo często widzę tutaj zamieszanie.

  1. Google Tag Manager (GTM) to nasz “kontener”. To on zarządza tym, co i kiedy ładuje się na stronie. Jego zaletą jest to, że raz wpięty w kod strony, pozwala nam zarządzać resztą z poziomu panelu UI, bez angażowania programistów.

  2. Google Analytics 4 (GA4) to tylko jedno z narzędzi (“tagów”), które wkładamy do tego kontenera.

Moja rekomendacja jest prosta: Strona ładuje tylko lekki kod GTM. Dopiero GTM decyduje, czy odpalić cięższą kobyłę w postaci GA4. Dzięki temu mamy kontrolę nad tym, kiedy zaczynamy śledzić użytkownika (hint: dopiero jak się zgodzi).

Strona Hugo
    └─ GTM (ładowany zawsze jako zarządca)
        └─ GA4 (ładowany warunkowo, po zgodzie)

Krok 1: Fundamenty (Konfiguracja kont)#

Zanim zacznę grzebać w kodzie, zawsze upewniam się, że mam przygotowane fundamenty. W tym przypadku sprowadza się to do wygenerowania dwóch kluczowych identyfikatorów, które będą naszymi punktami odniesienia w całym procesie.

  1. Google Analytics 4 (GA4): Najpierw utworzyłem nową usługę na analytics.google.com . To jest nasz cel, tutaj będą lądować wszystkie zebrane dane analityczne. Z tego kroku najważniejszy jest Measurement ID (identyfikator pomiaru), który ma format G-XXXXXXXX .

  2. Google Tag Manager (GTM): Następnie, na tagmanager.google.com, założyłem kontener typu “Web” . To z kolei jest nasz “panel sterowania” lub “kontener” na skrypty. Z tego miejsca pobrałem GTM ID (w formacie GTM-XXXXXXXX), który jako jedyny trafi bezpośrednio do kodu naszej strony .

Aby nie wracać do tego później, od razu w panelu GTM zrobiłem jedną, ważną rzecz: przygotowałem tag łączący GTM z GA4. Konfiguracja jest prosta: jako typ tagu wybrałem “Google Analytics: Konfiguracja GA4”, a następnie podałem Measurement ID uzyskany w pierwszym kroku . Jako regułę uruchamiającą (trigger) ustawiłem na razie All Pages. To celowe, tymczasowe uproszczenie, które pozwoli nam później zweryfikować, czy komunikacja w ogóle działa, zanim skomplikujemy logikę o zgody RODO.

Krok 2: Integracja z Hugo#

Tutaj zaczyna się właściwa praca z kodem. Zamiast wrzucać skrypty “na sztywno” w pliki motywu, użyłem partials. To pozwala na łatwiejsze utrzymanie kodu w przyszłości.

Struktura plików#

W katalogu layouts naszego projektu upewniłem się, że mamy następującą strukturę:

layouts/
├── partials/
│   ├── head-extended.html   <-- Tu wpinamy skrypt GTM
│   └── cookie-consent.html  <-- Nasz baner RODO
└── _default/
    └── baseof.html          <-- Główny layout

Konfiguracja i implementacja skryptów#

Zanim wkleimy jakikolwiek kod do szablonów, musimy zadbać o higienę projektu. Zamiast wpisywać identyfikator GTM “na sztywno” (hardcode) w plikach HTML, zdefiniowałem go jako parametr w pliku konfiguracyjnym hugo.toml.

To kluczowe z dwóch powodów: po pierwsze, oddzielamy konfigurację od kodu. Po drugie, pozwala nam to warunkowo ładować skrypty – jeśli usunę ID z konfiguracji (np. na środowisku lokalnym), skrypt po prostu się nie wyrenderuje .

W sekcji [params] dodałem zmienną:

[params]
  googleTagManagerID = "GTM-XXXXXXXX"

Teraz mogę bezpiecznie użyć tego parametru w szablonach. Jeśli używasz motywu, który wspiera extended_head.html, to jest to idealne miejsce. Zauważ, że cały blok kodu objąłem warunkiem if, to nasza “bramka bezpieczeństwa”.

{{ if $.Site.Params.googleTagManagerID }}
<!-- Google Tag Manager -->
<script>(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','{{ $.Site.Params.googleTagManagerID }}');</script>
<!-- End Google Tag Manager -->
{{ end }}

W ramach dobrych praktyk zadbałem również o użytkowników, którzy mają wyłączony JavaScript. Stworzyłem osobny plik gtm-noscript.html w katalogu partials i zaimplementowałem go w głównym szablonie baseof.html, zaraz po otwarciu znacznika <body>.

Tutaj również sprawdzam, czy ID jest zdefiniowane. Dzięki temu zachowujemy porządek w strukturze plików – główny layout pozostaje czytelny, a kod odpowiedzialny za obsługę <noscript> jest odseparowany i łatwy w utrzymaniu .

{{ if $.Site.Params.googleTagManagerID }}
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ $.Site.Params.googleTagManagerID }}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{{ end }}

Przejdźmy do najciekawszej części, czyli mechanizmu zgód. Mogliśmy oczywiście wpiąć gotowy, ciężki skrypt zewnętrzny, ale zależało mi na rozwiązaniu lekkim. Postawiłem na czysty JavaScript, bez zbędnych bibliotek typu jQuery.

Podczas implementacji podjąłem dwie kluczowe decyzje architektoniczne:

  1. Inline CSS dla wydajności. Zazwyczaj unikamy stylów w linii, ale tutaj zrobiłem wyjątek. Baner musi wyrenderować się natychmiast, a wrzucenie stylów bezpośrednio w div redukuje tzw. Critical Rendering Path. Nie zmuszamy przeglądarki do pobierania zewnętrznego arkusza CSS tylko po to, by pokazać prosty pasek .

  2. Centralizacja konfiguracji. Teksty trzymamy w odpowiednich plikcah tzw. tabelach tłumaczeń, a nie “na sztywno” w kodzie HTML. Dzięki temu obsługa wielojęzyczności (i18n) staje się trywialna, wystarczy dodać wpis w odpowiednim pliku konfiguracyjnym.

Oto jak wygląda przykładowa konfiguracja w i18n/pl.toml:

[cookieConsentMessage]
other = "Używamy plików cookies i Google Tag Manager do analizy i poprawy doświadczenia użytkownika."

[cookieConsentAccept]
other = "Akceptuj"

[cookieConsentDecline]
other = "Odrzuć"

Logika działania (JavaScript)#

Sercem całego rozwiązania jest plik layouts/partials/cookie-consent.html. Napisałem tam prosty skrypt, w którym najważniejszą rolę odgrywa funkcja notifyGTMConsent.

Dlaczego jest tak istotna? Ponieważ samo zapisanie ciasteczka w przeglądarce to za mało. Musimy aktywnie powiadomić o tym fakcie Google Tag Managera.

  <div id="cookie-consent-banner" style="{your_styles}">
  <div style="{your_styles}">
    <div style="flex: 1;">
      <p style="{your_styles}">
        {{ i18n "cookieConsentMessage" }}
      </p>
    </div>
    <div style="{your_styles}">
      <button id="cookie-decline" style="{your_styles}">
        {{ i18n "cookieConsentDecline" }}
      </button>
      <button id="cookie-accept" style="{your_styles}">
        {{ i18n "cookieConsentAccept" }}
      </button>
    </div>
  </div>
</div>

<script>
(function() {
  'use strict';

  const COOKIE_NAME = 'cookie-consent';
  const ANALYTICS_COOKIE = 'analytics-consent';
  const banner = document.getElementById('cookie-consent-banner');

  function getCookie(name) {
    const value = '; ' + document.cookie;
    const parts = value.split('; ' + name + '=');
    return parts.length === 2 ? parts.pop().split(';').shift() : null;
  }

  function setCookie(name, value, days = 365) {
    const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
    document.cookie = name + '=' + value + ';expires=' + expires.toUTCString() + ';path=/;SameSite=Lax';
  }

  function notifyGTMConsent() {
    if (window.dataLayer) {
      window.dataLayer.push({
        event: 'consent_update',
        analytics_consent: getCookie(ANALYTICS_COOKIE) === 'true'
      });
    }
  }

  function handleConsent(accepted) {
    setCookie(COOKIE_NAME, accepted ? 'accepted' : 'declined');
    setCookie(ANALYTICS_COOKIE, accepted ? 'true' : 'false');
    notifyGTMConsent();
    if (banner) banner.style.display = 'none';
  }

  // Setup event listeners FIRST (avoid race condition)
  if (banner) {
    const acceptBtn = document.getElementById('cookie-accept');
    const declineBtn = document.getElementById('cookie-decline');

    if (acceptBtn) acceptBtn.addEventListener('click', function() { 
      handleConsent(true);
    });
    if (declineBtn) declineBtn.addEventListener('click', function() {
      handleConsent(false);
    });
  }

  // THEN check consent and show banner if needed
  if (!getCookie(COOKIE_NAME)) {
    if (banner) banner.style.display = 'block';
  } else {
    notifyGTMConsent();
  }
})();
</script>

W kodzie zadbałem o dwa istotne detale, które zwiększają stabilność i bezpieczeństwo rozwiązania. Przede wszystkim wyeliminowałem potencjalny wyścig (race condition) poprzez specyficzną kolejność operacji: najpierw podpinam nasłuchiwanie zdarzeń na przyciskach, a dopiero w następnym kroku sprawdzam stan ciasteczka i decyduję o wyświetleniu banera. Dzięki temu mam pewność, że nawet przy błyskawicznym wykonaniu skryptu nie zgubimy interakcji użytkownika. Dodatkowo, przy samym ustawianiu ciasteczka wymusiłem flagę SameSite=Lax, co jest obecnie standardem w dbaniu o prywatność danych i zabezpiecza nas przed niechcianym przesyłaniem ciastek między witrynami.

Krok 4: Logika biznesowa w GTM (Triggers)#

Kiedy mamy gotowy kod na stronie, musimy poinstruować Google Tag Managera, jak ma interpretować sygnały płynące z naszej witryny. To jest ten moment, w którym spinamy wszystko w całość i tłumaczymy GTM-owi: “Hej, ładuj analitykę dopiero wtedy, gdy użytkownik da nam zielone światło”.

Aby to osiągnąć, w panelu GTM skonfigurowałem dwa elementy. Najpierw zdefiniowałem nową Zmienną warstwy danych (Data Layer Variable) o nazwie - analytics_consent. Jej zadaniem jest nasłuchiwanie i przechwytywanie wartości, którą nasz skrypt JavaScript wysyła do warstwy danych.

Mając zmienną, mogłem stworzyć właściwą Regułę (Trigger), którą nazwałem Cookie Consent - Analytics Approved. To tutaj dzieje się “magia” decyzyjna. Ustawiłem typ reguły na Zdarzenie niestandardowe (Custom Event) i skonfigurowałem ją tak, aby reagowała na zdarzenie consent_update, ale – i to jest kluczowe – tylko pod warunkiem, że nasza zmienna analytics_consent przyjmie wartość true. Dzięki temu mamy pewność, że tagi odpalą się wyłącznie po wyraźnej zgodzie użytkownika.

Finalna konfiguracja Tagu GA4#

Na sam koniec wróciłem do konfiguracji tagu GA4, aby połączyć kropki. Przypisałem mu dwa równoległe triggery, co może wydawać się na pierwszy rzut oka nadmiarowe, ale jest kluczowe dla poprawności działania. Pierwszy to standardowy Initialization - All Pages, który ładuje samą konfigurację kontenera. Drugi to nasz nowy Cookie Consent - Analytics Approved, który odpowiada za właściwe uruchomienie śledzenia.

Taka konfiguracja zapewnia nam pełne pokrycie wszystkich scenariuszy biznesowych:

  • Nowy użytkownik: Wchodzi na stronę, widzi baner i klika “Akceptuj”. W tym momencie nasz skrypt wysyła zdarzenie consent_update, które natychmiast uruchamia GA4. Nie tracimy sesji.
  • Powracający użytkownik: Wchodzi na stronę, a skrypt automatycznie wykrywa istniejące ciasteczko zgody. W tle leci consent_update, a GA4 startuje momentalnie, bez ponownego atakowania użytkownika banerem.
  • Użytkownik na “nie”: Skrypt wysyła sygnał false. Nasz trigger to widzi, nie spełnia warunku uruchomienia i blokuje wysyłkę danych. Mamy więc pełną zgodność z RODO bez zbędnej gimnastyki w kodzie .

Weryfikacja i wnioski końcowe#

Na koniec, zanim zamkniemy temat, zawsze przeprowadzam szybki “sanity check”. Musimy mieć pewność, że nasza logika zgód faktycznie działa, a nie tylko wygląda. Korzystam tu zazwyczaj z trzech metod weryfikacji.

  1. Konsola przeglądarki (DevTools). To pierwszy front walki. Po wejściu na stronę otwieram konsolę i wpisuję dataLayer. Pozwala mi to sprawdzić, czy tablica w ogóle istnieje i czy odkładają się w niej nasze zdarzenia. Jeśli widzę tam historię eventów, wiem, że komunikacja na linii strona-GTM działa poprawnie.

  2. GTM Preview Mode. To zdecydowanie najlepsze narzędzie do debugowania. W trybie podglądu widzę czarno na białym, które tagi się uruchomiły (status “Fired”), a które grzecznie czekają na zgodę użytkownika (status “Not Fired”). To tutaj ostatecznie potwierdzam, czy blokada RODO jest szczelna.

  3. DebugView w GA4. Tutaj mała uwaga dla mniej cierpliwych: standardowe raporty w Google Analytics mogą mieć opóźnienie sięgające nawet 24 godzin. Dlatego przy testach polegam wyłącznie na widoku “DebugView”, który pokazuje ruch w czasie rzeczywistym. Jeśli tam widzę dane, to znaczy, że wszystko jest w porządku.

Wdrożenie analityki w ten sposób daje nam dwie ogromne korzyści architektoniczne. Po pierwsze, utrzymujemy higienę w repozytorium Hugo, kod jest czysty i pozbawiony dziesiątek wklejanych “na szybko” skryptów śledzących. Po drugie, zyskujemy elastyczność. Gdy dział marketingu poprosi o dodanie taga Pinterest-a czy LinkedIn-a, obsłużymy to w panelu GTM w kilka minut, bez konieczności angażowania programistów i robienia nowego deploymentu strony.

Powodzenia przy wdrażaniu