This commit is contained in:
2026-05-02 08:59:49 +12:00
parent 5f6b47d445
commit 629f530099
8 changed files with 102 additions and 25 deletions
+8
View File
@@ -30,6 +30,14 @@
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
/>
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-K7TLSFJVP1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-K7TLSFJVP1');
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
+88
View File
@@ -0,0 +1,88 @@
type GtagFn = (...args: unknown[]) => void;
const TEXT_LIMIT = 80;
function gtag(): GtagFn | null {
if (typeof window === 'undefined') return null;
const fn = (window as unknown as { gtag?: GtagFn }).gtag;
return typeof fn === 'function' ? fn : null;
}
export function trackPageView(path: string, title: string): void {
const send = gtag();
if (!send) return;
send('event', 'page_view', {
page_path: path,
page_title: title,
page_location: window.location.href
});
}
function getLabel(el: HTMLElement): string {
const aria = el.getAttribute('aria-label');
if (aria) return aria.trim().slice(0, TEXT_LIMIT);
const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim();
if (text) return text.slice(0, TEXT_LIMIT);
const title = el.getAttribute('title');
if (title) return title.trim().slice(0, TEXT_LIMIT);
return '';
}
function getLocation(el: HTMLElement): string {
const dataLoc = el.closest<HTMLElement>('[data-track-location]');
if (dataLoc?.dataset.trackLocation) {
return dataLoc.dataset.trackLocation.slice(0, TEXT_LIMIT);
}
const section = el.closest<HTMLElement>('section[id]');
if (section?.id) return section.id;
if (el.closest('header')) return 'header';
if (el.closest('footer')) return 'footer';
if (el.closest('nav')) return 'nav';
return 'body';
}
export function initClickTracking(): () => void {
if (typeof window === 'undefined') return () => {};
const handler = (event: MouseEvent) => {
const send = gtag();
if (!send) return;
const target = event.target;
if (!(target instanceof Element)) return;
const interactive = target.closest<HTMLElement>('a, button, [role="button"]');
if (!interactive) return;
const isLink = interactive.tagName === 'A';
const label = getLabel(interactive);
const location = getLocation(interactive);
const params: Record<string, unknown> = {
element: isLink ? 'link' : 'button',
label,
location,
page_path: window.location.pathname
};
if (isLink) {
const href = (interactive as HTMLAnchorElement).href;
if (href) {
params.link_url = href;
try {
const url = new URL(href, window.location.href);
params.outbound = url.hostname !== window.location.hostname;
} catch {
params.outbound = false;
}
}
} else {
const btn = interactive as HTMLButtonElement;
if (btn.type) params.button_type = btn.type;
if (btn.name) params.button_name = btn.name;
}
send('event', isLink ? 'link_click' : 'button_click', params);
};
document.addEventListener('click', handler, { capture: true });
return () => document.removeEventListener('click', handler, { capture: true });
}
+6
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { afterNavigate, disableScrollHandling } from '$app/navigation';
import { initClickTracking, trackPageView } from '$lib/analytics';
import '$lib/styles/variables.css';
import '$lib/styles/base.css';
import '$lib/styles/layout.css';
@@ -9,6 +11,8 @@
import '$lib/styles/sections.css';
import '$lib/styles/responsive.css';
onMount(() => initClickTracking());
afterNavigate(({ from, to }) => {
if (!from || !to || to.url.hash) {
return;
@@ -28,6 +32,8 @@
document.body.scrollTop = 0;
});
});
trackPageView(to.url.pathname + to.url.search, document.title);
}
});
</script>