// gtm.jsx — GA4 + GTM conversion tracking framework for Green Integrations // =========================================================================== // Implementation of the tracking layer specified in // docs/GI_Link_Audit_and_Tracking_Reference.md (Part 3). // // Three pieces: // 1. window.GIDataLayer — tiny safe wrapper around dataLayer.push // 2. window.GIGtm — helpers for the four GA4 Key Events // 3. Global click delegate — reads data-gtm-event / -label / -location / // -stage attributes from every clicked CTA and fires the matching event // automatically. No per-component tag required. // // Wiring on the WordPress side: see includes/gtm-tracking.php — that file // outputs the GTM container snippet in and the noscript iframe in // . This JSX assumes window.dataLayer is already initialized there. // // Why a separate shared file? // * Centralizes the schema (event names, parameter names) — one place to // update if GTM tags change. // * Lets every page opt in by listing 'shared/jsx/gtm.jsx' in the page // registry's shared_jsx array. // * Keeps individual CTAs declarative: just slap data-gtm-* attrs on the // element and the global listener handles the rest. (function () { if (typeof window === 'undefined') return; // ---------- 0. Registry key -> audit-doc location + default stage ---------- // Source: docs/GI_Link_Audit_and_Tracking_Reference.md (Part 3, // "data-gtm-location Reference Table"). Keeping the mapping here means // a single edit propagates to every page automatically — individual CTAs // only need to override when their location differs from the page default // (e.g. footer CTA on a solution page should use 'footer-cta', not // 'sol-commercial-solar'). const LOCATION_MAP = { 'home': { location: 'homepage', stage: 'awareness' }, 'solutions-commercial-solar': { location: 'sol-commercial-solar', stage: 'decision' }, 'solutions-energy-audits': { location: 'sol-energy-audits', stage: 'decision' }, 'solutions-battery-storage': { location: 'sol-battery-storage', stage: 'decision' }, 'solutions-led-lighting': { location: 'sol-led-lighting', stage: 'decision' }, 'solutions-ev-charging': { location: 'sol-ev-charging', stage: 'decision' }, 'solutions-solar-maintenance': { location: 'sol-solar-maintenance', stage: 'decision' }, 'industries-manufacturing': { location: 'ind-manufacturing', stage: 'consideration'}, 'industries-food-and-beverage': { location: 'ind-food-beverage', stage: 'consideration'}, 'industries-warehousing': { location: 'ind-warehousing', stage: 'consideration'}, 'industries-agriculture': { location: 'ind-agriculture', stage: 'consideration'}, 'industries-real-estate': { location: 'ind-real-estate', stage: 'consideration'}, 'industries-government': { location: 'ind-government', stage: 'consideration'}, 'incentives': { location: 'res-incentives', stage: 'decision' }, 'payment-plans': { location: 'res-payment-plans', stage: 'decision' }, 'solar-calculator': { location: 'res-solar-calculator', stage: 'decision' }, 'blog': { location: 'blog-list', stage: 'consideration'}, 'blog-post': { location: 'blog-post-cta', stage: 'consideration'}, 'project-single': { location: 'project-single', stage: 'decision' }, 'projects': { location: 'projects-list', stage: 'consideration'}, 'our-story': { location: 'our-story', stage: 'awareness' }, 'partners': { location: 'partners', stage: 'consideration'}, 'careers': { location: 'careers', stage: 'awareness' }, 'contact': { location: 'contact-page', stage: 'decision' }, 'community': { location: 'community', stage: 'awareness' }, 'knowledge-centre': { location: 'qa-knowledge-centre', stage: 'consideration'}, 'qa-topic': { location: 'qa-topic', stage: 'consideration'}, 'question-answer': { location: 'qa-single', stage: 'consideration'} }; // Expose for the Button primitive and any caller that wants the per-page // defaults without re-implementing the map. window.GI_GTM_LOCATION_MAP = LOCATION_MAP; window.GI_GTM_PAGE_DEFAULTS = function () { const key = window.GI_PAGE; if (key && LOCATION_MAP[key]) return LOCATION_MAP[key]; return { location: 'unknown', stage: 'awareness' }; }; // ---------- 1. Safe dataLayer wrapper ---------- // GTM auto-initializes window.dataLayer; we guard for the case where the // container snippet hasn't loaded yet (e.g. in dev without GTM_ID set). window.dataLayer = window.dataLayer || []; const GIDataLayer = { push(payload) { if (!payload || typeof payload !== 'object') return; try { window.dataLayer.push(payload); // Surface to console in dev when ?gi_gtm_debug=1 is in the URL. if (typeof window !== 'undefined' && window.location && /[?&]gi_gtm_debug=1/.test(window.location.search)) { // eslint-disable-next-line no-console console.log('[GI dataLayer]', payload); } } catch (e) { // Never let analytics throw kill the page. } } }; // ---------- 2. Key Event helpers ---------- // Matches the four GA4 Key Events defined in Part 3 of the audit doc. // Each one is a thin wrapper that enforces parameter naming. const GIGtm = { // Fired on any tracked CTA click. Usually called automatically by the // global click listener below; exposed for cases where a component // needs to fire it manually (e.g. inside an onClick handler that // navigates programmatically). ctaClick({ text, location, destination, stage }) { GIDataLayer.push({ event: 'cta_click', cta_text: text || '', cta_location: location || 'unknown', cta_destination: destination || '', funnel_stage: stage || 'awareness' }); }, // Fire AFTER successful contact-form submission (not on click). generateLead({ formLocation }) { GIDataLayer.push({ event: 'generate_lead', form_location: formLocation || 'contact-form' }); }, // Fire on click of any tel: link or phone-number button. phoneCall({ source }) { GIDataLayer.push({ event: 'phone_call', click_source: source || 'unknown' }); }, // Fire when the solar calculator reaches its results step. solarCalcComplete({ kwh } = {}) { GIDataLayer.push({ event: 'solar_calc_complete', calc_result_kwh: typeof kwh === 'number' ? kwh : null }); }, // Fire on click of any PDF download anchor. pdfDownload({ fileName }) { GIDataLayer.push({ event: 'pdf_download', file_name: fileName || 'unknown' }); } }; // ---------- 3. Global click delegate ---------- // Reads data-gtm-* attrs off the *clicked* element OR the closest // ancestor that has them. Fires the appropriate event based on the // declared `data-gtm-event` value: // // data-gtm-event="cta_click" -> GIGtm.ctaClick // data-gtm-event="phone_call" -> GIGtm.phoneCall // data-gtm-event="pdf_download" -> GIGtm.pdfDownload // // Lead-generation and solar-calc events are fired imperatively from // their respective forms (see contact-form.jsx, solar-calc-calculator.jsx). // // Also auto-fires phone_call for any click, even // without explicit data-gtm-* attrs — phone clicks are too valuable to // miss because someone forgot the attribute. function findGtmHost(target) { let node = target; while (node && node !== document) { if (node.dataset && (node.dataset.gtmEvent || (node.tagName === 'A' && (node.getAttribute('href') || '').indexOf('tel:') === 0))) { return node; } node = node.parentNode; } return null; } function handleGlobalClick(e) { const host = findGtmHost(e.target); if (!host) return; const href = (host.getAttribute && host.getAttribute('href')) || ''; // Implicit tel: tracking (catches any phone link even without attrs). if (href.indexOf('tel:') === 0) { GIGtm.phoneCall({ source: (host.dataset && host.dataset.gtmLocation) || inferLocationFromPath() }); return; } const eventName = host.dataset && host.dataset.gtmEvent; if (!eventName) return; if (eventName === 'cta_click') { GIGtm.ctaClick({ text: host.dataset.gtmLabel || (host.textContent || '').trim().slice(0, 80), location: host.dataset.gtmLocation || inferLocationFromPath(), destination: href || host.dataset.gtmDestination || '', stage: host.dataset.gtmStage || inferStage() }); } else if (eventName === 'phone_call') { GIGtm.phoneCall({ source: host.dataset.gtmLocation || inferLocationFromPath() }); } else if (eventName === 'pdf_download') { GIGtm.pdfDownload({ fileName: host.dataset.gtmFileName || (href.split('/').pop() || '').replace(/\.[^.]+$/, '') || 'project-brief' }); } else if (eventName === 'generate_lead') { // Most callers should fire this AFTER successful submit, not on // click. Allow click-based wiring as an escape hatch for legacy // forms that don't have a success callback. GIGtm.generateLead({ formLocation: host.dataset.gtmLocation || inferLocationFromPath() }); } } // Last-ditch location inference for elements that forgot data-gtm-location. // Prefer LOCATION_MAP[window.GI_PAGE] so the auto-derived location matches // the docs' vocabulary exactly. Fall back to the registry key or URL path // for non-registry contexts (the latter shouldn't happen on a Claude page). function inferLocationFromPath() { if (window.GI_PAGE && LOCATION_MAP[window.GI_PAGE]) { return LOCATION_MAP[window.GI_PAGE].location; } if (window.GI_PAGE) return window.GI_PAGE; if (!window.location || !window.location.pathname) return 'unknown'; const p = window.location.pathname.replace(/\/+$/, ''); if (p === '' || p === '/') return 'homepage'; return p.split('/').filter(Boolean).join('-'); } function inferStage() { if (window.GI_PAGE && LOCATION_MAP[window.GI_PAGE]) { return LOCATION_MAP[window.GI_PAGE].stage; } return 'awareness'; } // Capture-phase so we run before React handlers stop propagation. document.addEventListener('click', handleGlobalClick, true); // ---------- Expose helpers globally ---------- window.GIDataLayer = GIDataLayer; window.GIGtm = GIGtm; })();