feat(analytics): add automatic CTA tracking to all 10 landing pages

Create Analytics.astro component in @manacore/shared-landing-ui that
automatically tracks CTA clicks and pricing section views via Umami.

The component uses event delegation and auto-detection of section
context (hero/pricing/footer) from section IDs or DOM position,
requiring zero changes to existing landing page content.

Tracked events: cta_click (with location), pricing_viewed,
pricing_plan_selected (with plan name)

Added to all 10 landing page Layout.astro files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-22 19:13:24 +01:00
parent 420926aef1
commit 287bbed86e
12 changed files with 155 additions and 0 deletions

View file

@ -0,0 +1,112 @@
---
/**
* Landing Page Analytics
*
* Add this component before </body> in your Layout.astro to automatically
* track CTA clicks and pricing section views via Umami.
*
* Works automatically with two approaches:
*
* 1. **Auto-detection** (zero config): Tracks all <a> and <button> clicks
* inside sections, inferring the location from the closest section's id
* or position on the page.
*
* 2. **Explicit attributes** (optional, more precise):
* - data-track-cta="hero|pricing|footer" → cta_click event
* - data-track-pricing="free|pro|team" → pricing_plan_selected event
* - data-track-section="pricing" → pricing_viewed on scroll
*
* @example
* ```astro
* <body>
* <slot />
* <Analytics />
* </body>
* ```
*/
---
<script>
function track(event: string, data?: Record<string, string | number | boolean>) {
if ((window as any).umami?.track) {
try {
(window as any).umami.track(event, data);
} catch {
// ignore
}
}
}
// Infer section location from element context
function getLocation(el: HTMLElement): string | null {
// 1. Check explicit data attribute
const explicit = el.closest('[data-track-cta]');
if (explicit) return (explicit as HTMLElement).dataset.trackCta || null;
// 2. Check closest section with id
const section = el.closest('section[id], div[id]');
if (section) {
const id = section.id.toLowerCase();
if (id.includes('hero')) return 'hero';
if (id.includes('pricing') || id.includes('plans')) return 'pricing';
if (id.includes('cta') || id.includes('download')) return 'footer';
if (id.includes('feature')) return 'features';
if (id.includes('faq')) return 'faq';
return id;
}
// 3. Check if it's in the first or last section of main
const main = document.querySelector('main');
if (main) {
const sections = main.querySelectorAll(':scope > *');
if (sections.length > 0) {
if (sections[0].contains(el)) return 'hero';
if (sections[sections.length - 1].contains(el)) return 'footer';
}
}
return null;
}
// Track CTA button/link clicks
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const link = target.closest('a[href], button') as HTMLElement | null;
if (!link) return;
// Skip navigation links (same-page anchors to sections)
const href = link.getAttribute('href') || '';
if (href.startsWith('#') && !href.includes('download')) return;
const location = getLocation(link);
if (!location) return;
track('cta_click', { location });
// Track pricing plan selection
const plan = link.dataset?.trackPricing;
if (plan) {
track('pricing_plan_selected', { plan });
}
});
// Track pricing section visibility
const pricingSection =
document.querySelector('[data-track-section="pricing"]') ||
document.querySelector('#pricing, #plans, [id*="pricing"]');
if (pricingSection) {
let tracked = false;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !tracked) {
tracked = true;
track('pricing_viewed');
observer.disconnect();
}
},
{ threshold: 0.3 }
);
observer.observe(pricingSection);
}
</script>