GA integration
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user