From 63cfbcdee16353abebf340b3b2b1c087ffcabd2d Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Wed, 4 Feb 2026 14:07:00 +0100 Subject: [PATCH] GA integration --- AGENTS.md | 40 +++++++++++++++++ assets/js/config.js | 5 ++- assets/js/main.js | 102 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e9860e4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Root HTML: `index.html`, `about.html`, `privacy.html`, `terms.html`, etc. +- `assets/`: site resources + - `assets/js/main.js`, `assets/js/config.js` + - `assets/css/style.css` + - `assets/images/`, `assets/icons/` +- `og-service/`: Go service generating Open Graph share pages (`/share`) and images (`/og`) with a `Makefile` and `README.md`. + +## Build, Test, and Development Commands +- Static site (serve locally): `python3 -m http.server 8000` in repo root, then open `http://localhost:8000/`. +- OG service: + - `cd og-service && make run` — run on `PORT` (default `8090`). + - `cd og-service && make build` — build to `og-service/bin/portfoliojournal-og`. + - `cd og-service && make clean` — remove build artifacts. + +## Coding Style & Naming Conventions +- HTML/CSS: 2-space indent; semantic markup; class names kebab-case. +- JavaScript: 2-space indent, semicolons, camelCase for variables/functions; avoid new globals (use `CONFIG` and module patterns). +- Go: follow `gofmt`; keep functions small, cohesive; prefer explicit names. + +## Testing Guidelines +- No unit tests in repo; validate manually. +- Static site: verify navigation highlighting, dark mode toggle, cookie notice, and lazy-loaded images. +- OG service quick check: + - `DATA='{"n":"Car","r":12500,"g":20000,"c":"USD","td":"2026-12-31"}'` + - `ENC=$(python3 - <<'PY'\nimport base64,sys;print(base64.urlsafe_b64encode(sys.stdin.read().encode()).decode().rstrip('='))\nPY <<< "$DATA")` + - `curl "http://localhost:8090/share?data=$ENC"` + - `curl -o og.png "http://localhost:8090/og?data=$ENC"` + +## Commit & Pull Request Guidelines +- Commits: concise, imperative subjects (e.g., `fix: correct dark mode label`). Conventional Commits encouraged. +- PRs: clear description, linked issues, before/after screenshots for UI, local test steps (site + OG endpoints), and any config changes noted. + +## Security & Configuration Tips +- Edit `assets/js/config.js` for site metadata; keep `ENABLE_ANALYTICS` false unless configured. +- `og-service` reads `PORT`; deploy behind a reverse proxy (see `og-service/README.md`). +- Do not commit secrets; use system service file `og-service/portfoliojournal-og.openrc` for Alpine/OpenRC setups. + diff --git a/assets/js/config.js b/assets/js/config.js index 3934971..7012145 100644 --- a/assets/js/config.js +++ b/assets/js/config.js @@ -12,8 +12,9 @@ const CONFIG = { WEBSITE_URL: 'https://portfoliojournal.app', EFFECTIVE_DATE: 'today', COPYRIGHT_YEAR: new Date().getFullYear(), - ENABLE_ANALYTICS: false, - ANALYTICS_ID: '', // Add your analytics ID if enabled + ENABLE_ANALYTICS: true, + ANALYTICS_ID: 'G-R3YRQZ8XZ7', // GA4 Measurement ID + ANALYTICS_DEBUG: false, // When true, logs events and marks GA events for DebugView // Social links (optional) SOCIAL: { diff --git a/assets/js/main.js b/assets/js/main.js index ae7195b..a56de35 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -7,8 +7,18 @@ 'use strict'; function trackEvent(eventName, props) { - if (window.plausible) { - window.plausible(eventName, { props: props || {} }); + const params = Object.assign({}, props || {}); + if (CONFIG.ANALYTICS_DEBUG) { + params.debug_mode = true; // Highlight in GA4 DebugView + try { console.debug('[analytics] event', eventName, params); } catch (_) {} + } + // GA4 (gtag) event + if (typeof window.gtag === 'function') { + try { window.gtag('event', eventName, params); } catch (_) {} + } + // Plausible (if present) + if (typeof window.plausible === 'function') { + try { window.plausible(eventName, { props: params }); } catch (_) {} } } @@ -60,6 +70,69 @@ } }; + // Google Analytics (GA4) Loader — gated by cookie consent + const AnalyticsLoader = { + loaded: false, + init() { + if (!CONFIG.ENABLE_ANALYTICS) return; + try { + const consent = localStorage.getItem('pj-cookies-accepted'); + if (consent === 'true') { + this.load(); + } + } catch (err) { + // ignore storage errors + } + }, + load() { + if (this.loaded) return; + if (!CONFIG.ANALYTICS_ID) return; + this.loaded = true; + + // Define dataLayer and gtag shim before loading script + window.dataLayer = window.dataLayer || []; + function gtag(){ window.dataLayer.push(arguments); } + window.gtag = gtag; + + gtag('js', new Date()); + // Consent granted for analytics after explicit acceptance + gtag('consent', 'update', { + ad_storage: 'denied', + analytics_storage: 'granted', + functionality_storage: 'granted', + security_storage: 'granted' + }); + gtag('config', CONFIG.ANALYTICS_ID, { + anonymize_ip: true, + send_page_view: false + }); + + const s = document.createElement('script'); + s.async = true; + s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(CONFIG.ANALYTICS_ID); + document.head.appendChild(s); + // Fire a page_view once available + if (document.readyState === 'complete') { + this.pageView(); + } else { + window.addEventListener('load', () => this.pageView(), { once: true }); + } + }, + pageView() { + if (!this.loaded || typeof window.gtag !== 'function') return; + const params = { + page_title: document.title, + page_location: window.location.href, + page_path: window.location.pathname + }; + if (CONFIG.ANALYTICS_DEBUG) params.debug_mode = true; + try { + window.gtag('event', 'page_view', params); + if (CONFIG.ANALYTICS_DEBUG) console.debug('[analytics] page_view', params); + } catch (_) {} + } + }; + // Navigation Management const Navigation = { init() { @@ -382,7 +455,10 @@ accept() { localStorage.setItem(this.STORAGE_KEY, 'true'); this.hide(); - // Initialize analytics here if needed + // Initialize GA4 after consent + if (typeof AnalyticsLoader !== 'undefined') { + AnalyticsLoader.load(); + } }, decline() { @@ -431,6 +507,25 @@ // Initialize everything on DOM ready document.addEventListener('DOMContentLoaded', () => { + // Initialize analytics (if enabled and consented previously) + if (typeof AnalyticsLoader !== 'undefined') { + AnalyticsLoader.init(); + } + // Track SPA-style navigation changes (pushState/replaceState/popstate) + (function setupSPAViews() { + const origPush = history.pushState; + const origReplace = history.replaceState; + function handleNav(){ + // Minimal debounce + clearTimeout(handleNav._tid); + handleNav._tid = setTimeout(() => { + if (typeof AnalyticsLoader !== 'undefined') AnalyticsLoader.pageView(); + }, 0); + } + history.pushState = function() { const r = origPush.apply(this, arguments); handleNav(); return r; }; + history.replaceState = function() { const r = origReplace.apply(this, arguments); handleNav(); return r; }; + window.addEventListener('popstate', handleNav); + })(); DarkMode.init(); DarkMode.updateToggleButton(); Navigation.init(); @@ -456,4 +551,5 @@ window.DarkMode = DarkMode; window.CookieNotice = CookieNotice; window.trackEvent = trackEvent; + window.AnalyticsLoader = AnalyticsLoader; })();