Instrukcja Wdrożenia Logowania Zgód Cookie (GDPR Compliance) na GitHub Pages

Niniejsza instrukcja opisuje proces implementacji mechanizmu logowania zgód użytkowników do zewnętrznego arkusza Google Sheets z wykorzystaniem biblioteki vanilla-cookieconsent (wersja 3.0). Rozwiązanie jest dostosowane do stron statycznych hostowanych na GitHub Pages.

1. Konfiguracja Backend (Google Apps Script)

Ten krok tworzy “serwer”, który będzie odbierał informacje o zgodach.

  1. Utwórz nowy arkusz Google Sheets.
  2. W pierwszym wierszu nadaj następujące nagłówki kolumnom:
  3. W menu wybierz Rozszerzenia -> Apps Script.
  4. Wklej poniższy kod do edytora skryptów:
// Dozwolona domena i kategorie zgód – odrzucamy śmieciowe żądania spoza strony
var ALLOWED_SITE = 'aitrain.pl';
var ALLOWED_CATEGORIES = ['necessary', 'analytics'];

// Ochrona przed formula injection w Google Sheets: wymusza tekst, przycina
// długość i neutralizuje znaki rozpoczynające formułę (=, +, -, @).
// Bez tego ktoś mógłby wysłać np. userAgent "=IMPORTXML(...)" i wykonać
// formułę przy otwarciu arkusza.
function sanitize(value, maxLength) {
  var s = String(value === null || value === undefined ? '' : value).substring(0, maxLength);
  if (/^[=+\-@]/.test(s)) {
    s = "'" + s;
  }
  return s;
}

function doPost(e) {
  try {
    var data = JSON.parse(e.postData.contents);

    // Walidacja: tylko zgody z naszej domeny
    if (data.site !== ALLOWED_SITE) {
      return ContentService.createTextOutput("Ignored").setMimeType(ContentService.MimeType.TEXT);
    }

    // Tylko znane kategorie zgód
    var accepted = (Array.isArray(data.accepted) ? data.accepted : [])
      .filter(function (c) { return ALLOWED_CATEGORIES.indexOf(c) !== -1; });

    var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
    sheet.appendRow([
      new Date(),
      sanitize(data.site, 100),
      sanitize(data.consentId, 100),
      sanitize(accepted.join(', '), 100),
      sanitize(data.userAgent, 300),
      sanitize(data.policyVersion, 20)
    ]);

    return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);
  } catch(err) {
    return ContentService.createTextOutput("Error").setMimeType(ContentService.MimeType.TEXT);
  }
}

Uwaga bezpieczeństwa: endpoint jest publiczny (każdy zna URL ze źródła strony), dlatego powyższy kod waliduje domenę i kategorie oraz neutralizuje próby wstrzyknięcia formuł do arkusza. Po każdej zmianie kodu w Apps Script należy ponownie wdrożyć aplikację: Wdróż -> Zarządzaj wdrożeniami -> ikona ołówka -> Wersja: Nowa wersja -> Wdróż (URL pozostaje bez zmian).

  1. Kliknij Wdróż (Deploy) -> Nowe wdrożenie (New deployment).
  2. Wybierz typ: Aplikacja internetowa (Web app).
  3. Ustaw:
  4. Skopiuj wygenerowany URL aplikacji internetowej (Web app URL). Będzie potrzebny w następnym kroku.

Utwórz plik w swoim projekcie, np. w katalogu _includes/cookie-banner.html (dla Jekyll) lub po prostu jako fragment kodu do wklejenia przed zamknięciem </body>.

Wklej poniższy kod, podmieniając TWOJ_URL_APPS_SCRIPT na adres skopiowany w punkcie 1.

<!-- Vanilla CookieConsent Stylesheet -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@v3.0.0/dist/cookieconsent.css">
<!-- Vanilla CookieConsent Script -->
<script src="https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@v3.0.0/dist/cookieconsent.umd.js"></script>

<script>
  // --- KONFIGURACJA ---
  // ID Twojego Google Analytics (zostaw puste lub usuń logikę GA jeśli nie używasz)
  const GA_MEASUREMENT_ID = 'G-XXXXXXXXXX'; 
  // Wklej tutaj URL wygenerowany w Google Apps Script
  const LOGGING_ENDPOINT = 'TWOJ_URL_APPS_SCRIPT';
  // Data ostatniej aktualizacji Polityki Prywatności
  const POLICY_VERSION = '2026-01-04'; 


  // --- FUNKCJE POMOCNICZE ---

  // Funkcja ładująca skrypt GA
  function loadGoogleAnalytics() {
    if (!GA_MEASUREMENT_ID) return;
    if (document.querySelector('script[src*="googletagmanager.com/gtag/js"]')) return;

    var s = document.createElement('script');
    s.async = true;
    s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(GA_MEASUREMENT_ID);
    document.head.appendChild(s);

    var init = document.createElement('script');
    init.innerHTML = "window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '" + GA_MEASUREMENT_ID + "');";
    document.head.appendChild(init);
  }

  // Funkcja aktualizująca Google Consent Mode
  function updateConsentMode(granted) {
    if (typeof gtag === 'function') {
      gtag('consent', 'update', {
        'ad_storage': granted ? 'granted' : 'denied',
        'analytics_storage': granted ? 'granted' : 'denied',
        'functionality_storage': granted ? 'granted' : 'denied',
        'personalization_storage': granted ? 'granted' : 'denied',
        'security_storage': 'granted'
      });
    }
  }

  // Funkcja logująca zgody do Google Sheets
  const logConsent = (cookie) => {
    if (!LOGGING_ENDPOINT) return;
    try {
        fetch(LOGGING_ENDPOINT, {
            method: 'POST',
            mode: 'no-cors', // Ważne dla cross-origin do Google
            body: JSON.stringify({
                site: window.location.hostname,
                consentId: cookie.consentId,
                accepted: cookie.categories,
                userAgent: navigator.userAgent,
                policyVersion: POLICY_VERSION
            })
        });
    } catch (e) {
        console.error('Consent logging failed', e);
    }
  };

  // --- INICJALIZACJA COOKIE CONSENT ---

  CookieConsent.run({
    guiOptions: {
      consentModal: {
        layout: 'box',
        position: 'bottom right',
        equalWeightButtons: true,
        flipButtons: false
      },
      preferencesModal: {
        layout: 'box',
        position: 'right',
        equalWeightButtons: true,
        flipButtons: false
      }
    },
    categories: {
      necessary: {
        readOnly: true
      },
      analytics: {}
    },
    language: {
      default: 'pl',
      autoDetect: 'browser',
      translations: {
        pl: {
          consentModal: {
            title: 'Szanujemy Twoją prywatność',
            description: 'Używamy plików cookie do analityki i personalizacji zgodnie z Consent Mode v2. Możesz zaakceptować wszystkie lub dostosować wybór.',
            acceptAllBtn: 'Akceptuj wszystkie',
            acceptNecessaryBtn: 'Odrzuć opcjonalne',
            showPreferencesBtn: 'Zarządzaj opcjami'
          },
          preferencesModal: {
            title: 'Ustawienia plików cookie',
            acceptAllBtn: 'Akceptuj wszystkie',
            acceptNecessaryBtn: 'Odrzuć opcjonalne',
            savePreferencesBtn: 'Zapisz ustawienia',
            closeIconLabel: 'Zamknij',
            serviceCounterLabel: 'Usługi',
            sections: [
              {
                title: 'Niezbędne pliki cookie',
                description: 'Te pliki są wymagane do poprawnego działania strony i zapewnienia bezpieczeństwa.',
                linkedCategory: 'necessary'
              },
              {
                title: 'Analityka i personalizacja',
                description: 'Pliki cookie pozwalające nam analizować ruch na stronie.',
                linkedCategory: 'analytics'
              }
            ]
          }
        }
      }
    },
    // Callback podczas pierwszego wyrażenia zgody
    onFirstConsent: ({ cookie }) => {
      logConsent(cookie); // Logowanie
      
      if (CookieConsent.acceptedCategory('analytics')) {
        updateConsentMode(true);
        loadGoogleAnalytics();
      } else {
        updateConsentMode(false);
      }
    },
    // Callback podczas kolejnych wizyt (opcjonalne, tutaj logika ta sama co wyżej)
    onConsent: ({ cookie }) => {
      if (CookieConsent.acceptedCategory('analytics')) {
        updateConsentMode(true);
        loadGoogleAnalytics();
      }
    },
    // Callback podczas zmiany ustawień przez użytkownika
    onChange: ({ changedCategories, cookie }) => {
      logConsent(cookie); // Logowanie zmiany
      
      if (CookieConsent.acceptedCategory('analytics')) {
        updateConsentMode(true);
        loadGoogleAnalytics();
      } else {
        updateConsentMode(false);
      }
    }
  });

  // Obsługa przycisku w footerze otwierającego ustawienia
  document.addEventListener('DOMContentLoaded', function () {
    document.body.addEventListener('click', function (e) {
      if (e.target.matches('[data-cc="c-settings"], [data-cc="c-settings"] *')) {
        e.preventDefault();
        CookieConsent.showPreferences();
      }
    });
  });
</script>

3. Implementacja w układzie strony (Layout)

Upewnij się, że powyższy plik jest dołączony do każdego szablonu strony, najlepiej tuż przed zamknięciem znacznika </body>.

W Jekyll (_layouts/default.html):

<body>
    <!-- Treść strony -->
    
    <footer id="footer">
    <div class="container">
        <div class="row">
            <div class="col-md-6">
                <h4 class="footer-heading">Kontakt</h4>
                <p><i class="fa fa-phone"></i> +48 691 300 548</p>
                <p><i class="fa fa-envelope"></i> kontakt@aitrain.pl</p>
                <p><i class="fa fa-check"></i> NIP: 583-154-14-45</p>
            </div>
            <div class="col-md-6">
                <h4 class="footer-heading">Nawigacja</h4>
                <div class="footer-links">
                    <a href="/">Strona główna</a> &nbsp;|&nbsp;
                    <a href="/#services">Usługi</a> &nbsp;|&nbsp;
                    <a href="/#portfolio-top">Portfolio</a> &nbsp;|&nbsp;
                    <a href="/#trainings">Szkolenia</a> &nbsp;|&nbsp;
                    <a href="/#contact">Kontakt</a> &nbsp;|&nbsp;
                    <a href="/polityka-prywatnosci/">Polityka prywatności</a> &nbsp;|&nbsp;
                    <a href="#" data-cc="c-settings">Ustawienia cookies</a>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-lg-12 text-center">
                <hr class="light">
                <p class="copyright">&copy; 2026 AITRAIN | Wszelkie prawa zastrzeżone</p>
            </div>
        </div>
    </div>
</footer>
    
    <!-- Skrypty JS -->
    
    <!-- Vanilla CookieConsent -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@v3.0.0/dist/cookieconsent.css"
  integrity="sha384-9KcNFt4axT+TNOVPHpwGHOm4Kv0UcMjdVya1kl+EPBQG+Vmqtz28cnx0f5PZYcnF" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@v3.0.0/dist/cookieconsent.umd.js"
  integrity="sha384-zM9A7bFtKjzCrGlAqAv3bj2Rkl6zz3vc62NMGZaEtfG4TKk8Ljrx6P7ixfoLzzIl" crossorigin="anonymous"></script>

<script>
  // Funkcja ładująca skrypt GA
  function loadGoogleAnalytics() {
    var gaId = 'G-L7R7ST44Q3';
    if (!gaId) return;

    // Sprawdź czy już nie załadowany
    if (document.querySelector('script[src*="googletagmanager.com/gtag/js"]')) return;

    var s = document.createElement('script');
    s.async = true;
    s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(gaId);
    document.head.appendChild(s);

    var init = document.createElement('script');
    init.innerHTML = "window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '" + gaId + "');";
    document.head.appendChild(init);
  }

  // Funkcja aktualizująca Consent Mode
  function updateConsentMode(granted) {
    if (typeof gtag === 'function') {
      gtag('consent', 'update', {
        'ad_storage': granted ? 'granted' : 'denied',
        'analytics_storage': granted ? 'granted' : 'denied',
        'functionality_storage': granted ? 'granted' : 'denied',
        'personalization_storage': granted ? 'granted' : 'denied',
        'security_storage': 'granted'
      });
    }
  }

  // Zawsze aktualizuj tę datę po zmianach w treści polityki prywatności
  const POLICY_VERSION = '2026-06-11';

  const logConsent = (cookie) => {
    try {
      const URL = 'https://script.google.com/macros/s/AKfycbyrnJUqxRQzZWBddD9bT6NR6nXchJQwsEooJUmt4Vjmk8_gSnY6LufQyn8z3C9uZQ37/exec';

      fetch(URL, {
        method: 'POST',
        mode: 'no-cors',
        body: JSON.stringify({
          site: window.location.hostname,
          consentId: cookie.consentId,
          accepted: cookie.categories,
          userAgent: navigator.userAgent,
          policyVersion: POLICY_VERSION
        })
      });
    } catch (e) {
      // Ciche logowanie błędu, aby nie straszyć użytkownika
      console.error('Consent logging failed', e);
    }
  };

  CookieConsent.run({
    guiOptions: {
      consentModal: {
        layout: 'box',
        position: 'bottom right',
        equalWeightButtons: true,
        flipButtons: false
      },
      preferencesModal: {
        layout: 'box',
        position: 'right',
        equalWeightButtons: true,
        flipButtons: false
      }
    },
    categories: {
      necessary: {
        readOnly: true
      },
      analytics: {}
    },
    language: {
      default: 'pl',
      autoDetect: 'browser',
      translations: {
        pl: {
          consentModal: {
            title: 'Szanujemy Twoją prywatność',
            description: 'Używamy plików cookie do analityki i personalizacji zgodnie z Consent Mode v2. Możesz zaakceptować wszystkie lub dostosować wybór.',
            acceptAllBtn: 'Akceptuj wszystkie',
            acceptNecessaryBtn: 'Odrzuć opcjonalne',
            showPreferencesBtn: 'Zarządzaj opcjami'
          },
          preferencesModal: {
            title: 'Ustawienia plików cookie',
            acceptAllBtn: 'Akceptuj wszystkie',
            acceptNecessaryBtn: 'Odrzuć opcjonalne',
            savePreferencesBtn: 'Zapisz ustawienia',
            closeIconLabel: 'Zamknij',
            serviceCounterLabel: 'Usługi',
            sections: [
              {
                title: 'Niezbędne pliki cookie',
                description: 'Te pliki są wymagane do poprawnego działania strony i zapewnienia bezpieczeństwa.',
                linkedCategory: 'necessary'
              },
              {
                title: 'Analityka i personalizacja',
                description: 'Pliki cookie pozwalające nam analizować ruch na stronie i dostosowywać treści do Twoich potrzeb (Google Analytics).',
                linkedCategory: 'analytics',
                cookieTable: {
                  headers: {
                    name: 'Nazwa',
                    domain: 'Serwis',
                    desc: 'Opis',
                    expiration: 'Wygasanie'
                  },
                  body: [
                    {
                      name: '_ga',
                      domain: 'google-analytics.com',
                      desc: 'Używany do rozróżniania użytkowników.',
                      expiration: '2 lata'
                    },
                    {
                      name: '_ga_*',
                      domain: 'google-analytics.com',
                      desc: 'Przechowuje stan sesji.',
                      expiration: '2 lata'
                    }
                  ]
                }
              }
            ]
          }
        }
      }
    },
    onFirstConsent: ({ cookie }) => {
      // NOWE: Logowanie zgody
      logConsent(cookie);
      if (CookieConsent.acceptedCategory('analytics')) {
        updateConsentMode(true);
        loadGoogleAnalytics();
      } else {
        updateConsentMode(false);
      }
    },
    onConsent: ({ cookie }) => {
      // Logika dla ponownych wejść (jeśli potrzebna, tutaj to samo co przy zmianie)
      if (CookieConsent.acceptedCategory('analytics')) {
        updateConsentMode(true);
        loadGoogleAnalytics();
      }
    },
    onChange: ({ changedCategories, cookie }) => {
      // NOWE: Logowanie zmiany zgody
      logConsent(cookie);
      if (CookieConsent.acceptedCategory('analytics')) {
        updateConsentMode(true);
        loadGoogleAnalytics();
      } else {
        // Jeśli użytkownik wycofał zgodę
        updateConsentMode(false);
        // Uwaga: wycofanie zgody w GA nie usuwa załadowanego skryptu, ale tagi przestaną wysyłać dane user-specific.
        // Można ewentualnie przeładować stronę: window.location.reload();
      }
    }
  });

  // Manual binding for setting links to ensure they work
  document.addEventListener('DOMContentLoaded', function () {
    document.body.addEventListener('click', function (e) {
      if (e.target.matches('[data-cc="c-settings"], [data-cc="c-settings"] *')) {
        e.preventDefault();
        CookieConsent.showPreferences();
      }
    });
  });
</script>
</body>

Aby użytkownik mógł w każdej chwili zmienić swoje zgody, dodaj w stopce (_includes/footer.html) link ze specjalnym atrybutem data-cc="c-settings". Skrypt banneru automatycznie wykryje kliknięcie w ten element i otworzy okno preferencji.

<a href="#" data-cc="c-settings">Ustawienia cookies</a>

To wszystko! Po publikacji strony, każde zaakceptowanie lub zmiana zgód przez użytkownika wyśle nowy wiersz do Twojego arkusza Google Sheets.

Kontakt


Skontaktuj się ze mną, a odpowiem tak szybko, jak to możliwe!

Telefon:

+48 691 300 548

NIP: 583-154-14-45