GA integration

This commit is contained in:
alexandrev-tibco
2026-02-04 14:07:00 +01:00
parent 379f935fe7
commit 63cfbcdee1
3 changed files with 142 additions and 5 deletions
+40
View File
@@ -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.
+3 -2
View File
@@ -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: {
+99 -3
View File
@@ -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;
})();