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

@ -1,5 +1,6 @@
---
import '../styles/global.css';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title?: string;
@ -52,5 +53,6 @@ const {
</head>
<body class="antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -1,5 +1,6 @@
---
import '../styles/global.css';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title: string;
@ -57,5 +58,6 @@ const {
</head>
<body class="min-h-screen bg-background-page text-text-primary antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -1,5 +1,6 @@
---
import '../styles/global.css';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title?: string;
@ -52,5 +53,6 @@ const {
</head>
<body class="antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -1,6 +1,7 @@
---
import AlternateLinks from '../components/seo/AlternateLinks.astro';
import Sidebar from '../components/navigation/Sidebar.astro';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
import { getLangFromUrl } from '../lib/i18n/config';
export interface Props {
@ -138,6 +139,7 @@ const lang = getLangFromUrl(Astro.url);
<!-- FAB Sidebar Navigation -->
<Sidebar />
<Analytics />
</body>
</html>

View file

@ -1,5 +1,6 @@
---
import '../styles/global.css';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title: string;
@ -57,5 +58,6 @@ const {
</head>
<body class="min-h-screen bg-background-page text-text-primary antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -1,4 +1,6 @@
---
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title: string;
}
@ -54,5 +56,6 @@ const { title } = Astro.props;
</head>
<body>
<slot />
<Analytics />
</body>
</html>

View file

@ -1,4 +1,6 @@
---
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title: string;
description?: string;
@ -38,6 +40,7 @@ const { title, description = 'NutriPhi - KI-gestützte Ernährungsanalyse per Fo
</head>
<body class="bg-[#0F1F0F] text-gray-100 antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -3,6 +3,7 @@ import '../styles/global.css';
// import { HeadHrefLangs } from 'astro-i18next/components';
import { t } from '../i18n';
import LanguageSwitcher from '@components/LanguageSwitcher.astro';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title?: string;
@ -55,6 +56,7 @@ const {
</div>
<slot />
<Analytics />
</body>
</html>

View file

@ -1,5 +1,6 @@
---
import '../styles/global.css';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title: string;
@ -57,5 +58,6 @@ const {
</head>
<body class="min-h-screen bg-background-page text-text-primary antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -1,5 +1,6 @@
---
import '../styles/global.css';
import Analytics from '@manacore/shared-landing-ui/atoms/Analytics.astro';
interface Props {
title?: string;
@ -52,5 +53,6 @@ const {
</head>
<body class="antialiased">
<slot />
<Analytics />
</body>
</html>

View file

@ -88,6 +88,27 @@ Diese Events erfordern **keinen Code in den einzelnen Apps** — sie werden auto
---
## Landing Page Event Tracking
Alle Landing Pages binden `<Analytics />` aus `@manacore/shared-landing-ui` ein. Das Script trackt automatisch:
| Event | Wann | Data |
|-------|------|------|
| `cta_click` | Klick auf CTA-Button/Link | `{ location: 'hero' \| 'pricing' \| 'footer' \| ... }` |
| `pricing_viewed` | Pricing-Section wird sichtbar | - |
| `pricing_plan_selected` | Klick auf Pricing-Plan CTA | `{ plan: 'free' \| 'pro' \| ... }` |
**Auto-Detection:** Das Script erkennt die Section automatisch aus `id`-Attributen oder der Position im DOM (erster/letzter Abschnitt).
**Explizite Attribute (optional):**
```html
<a href="/register" data-track-cta="hero">Jetzt starten</a>
<a href="/pro" data-track-cta="pricing" data-track-pricing="pro">Pro starten</a>
<section data-track-section="pricing">...</section>
```
---
## Custom Event Tracking
### Installation

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>