cms.c2sgmbh/docs/anleitungen/Analytics.md
Martin Porwoll ba0f37a5b2 docs(analytics): update URLs to use production endpoints
- Update Umami URLs from internal IPs to production (analytics.c2sgmbh.de)
- Add Development vs Production URL comparison table
- Update UmamiScript component to use production URL as default
- Add Payload CMS API URLs to frontend .env.local example
- Update server-side tracking to use production Umami
- Add global type declarations for gtag and CookieConsent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 16:03:45 +00:00

30 KiB

ANALYTICS-LÖSUNG: Implementierungsübersicht für Payload CMS

Letzte Aktualisierung: 18. Dezember 2025

Kontext

Du entwickelst das Multi-Tenant Payload CMS Backend und Next.js Frontend für 4 Websites. Diese Dokumentation beschreibt die Analytics-Lösung, die in das Frontend integriert werden muss.

Wichtig: Frontends verwenden die Production-API (cms.c2sgmbh.de) und Production-Analytics (analytics.c2sgmbh.de).


Architektur-Übersicht

┌─────────────────────────────────────────────────────────────────────────────────────┐
│  ANALYTICS ARCHITEKTUR                                                              │
│                                                                                     │
│  ┌─────────────────────────────────────────────────────────────────────────────┐   │
│  │                         OHNE CONSENT (immer aktiv)                          │   │
│  │                                                                             │   │
│  │   ┌─────────────────────────────────────────────────────────────────────┐   │   │
│  │   │                      UMAMI ANALYTICS                                │   │   │
│  │   │                                                                     │   │   │
│  │   │   Server:    sv-analytics (10.10.181.103:3000)                      │   │   │
│  │   │   Dashboard: http://10.10.181.103:3000                              │   │   │
│  │   │   Script:    /custom.js (Anti-Adblock)                              │   │   │
│  │   │   Endpoint:  /api/send                                              │   │   │
│  │   │                                                                     │   │   │
│  │   │   Features:                                                         │   │   │
│  │   │   • Cookieless Tracking (DSGVO-konform ohne Einwilligung)          │   │   │
│  │   │   • Pageviews, Sessions, Referrer, UTM-Parameter                   │   │   │
│  │   │   • Custom Events (Newsletter, Formulare, CTAs, Downloads)         │   │   │
│  │   │   • 100% Erfassung aller Besucher                                  │   │   │
│  │   └─────────────────────────────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────────────────────┘   │
│                                                                                     │
│  ┌─────────────────────────────────────────────────────────────────────────────┐   │
│  │                    MIT CONSENT (Kategorie: "marketing")                     │   │
│  │                                                                             │   │
│  │   ┌─────────────────────────────────────────────────────────────────────┐   │   │
│  │   │                    GOOGLE ADS CONVERSION                            │   │   │
│  │   │                                                                     │   │   │
│  │   │   Client-Side (bei Consent):                                        │   │   │
│  │   │   • Google Ads Tag (gtag.js)                                        │   │   │
│  │   │   • Conversion Tracking                                             │   │   │
│  │   │   • Remarketing Audiences                                           │   │   │
│  │   │                                                                     │   │   │
│  │   │   Server-Side (immer, anonymisiert):                                │   │   │
│  │   │   • Google Ads Conversion API                                       │   │   │
│  │   │   • Enhanced Conversions (gehashte E-Mail)                          │   │   │
│  │   │   • GCLID-basierte Attribution                                      │   │   │
│  │   └─────────────────────────────────────────────────────────────────────┘   │   │
│  │                                                                             │   │
│  │   ┌─────────────────────────────────────────────────────────────────────┐   │   │
│  │   │                  GOOGLE CONSENT MODE v2                             │   │   │
│  │   │                                                                     │   │   │
│  │   │   Integration mit bestehendem Orestbida Consent-Banner              │   │   │
│  │   │   Kategorie "marketing" steuert:                                    │   │   │
│  │   │   • ad_storage                                                      │   │   │
│  │   │   • ad_user_data                                                    │   │   │
│  │   │   • ad_personalization                                              │   │   │
│  │   └─────────────────────────────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────────────────────┘   │
│                                                                                     │
└─────────────────────────────────────────────────────────────────────────────────────┘

Infrastruktur

Umami Server

Umgebung Server URL Dashboard
Production Hetzner 3 https://analytics.c2sgmbh.de https://analytics.c2sgmbh.de
Development sv-analytics (LXC 703) https://umami.porwoll.tech https://umami.porwoll.tech
Eigenschaft Production Development
Tracking Script https://analytics.c2sgmbh.de/script.js https://umami.porwoll.tech/script.js
API Endpoint https://analytics.c2sgmbh.de/api/send https://umami.porwoll.tech/api/send
Datenbank umami_db auf Hetzner 3 umami_db auf sv-postgres

Website-IDs (Multi-Tenant)

Die Website-IDs werden in Umami für jeden Tenant erstellt:

// src/config/analytics.ts

export const UMAMI_WEBSITE_IDS: Record<string, string> = {
  'porwoll.de': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
  'complexcaresolutions.de': 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy',
  'gunshin.de': 'zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz',
  'zweitmeinu.ng': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
}

// Production URL für Frontends
export const UMAMI_HOST = process.env.NEXT_PUBLIC_UMAMI_HOST || 'https://analytics.c2sgmbh.de'

Frontend-Integration

1. Umami Script Komponente

// src/components/analytics/UmamiScript.tsx

'use client'

import Script from 'next/script'

interface UmamiScriptProps {
  websiteId: string
  host?: string
}

export function UmamiScript({
  websiteId,
  host = 'https://analytics.c2sgmbh.de'  // Production default
}: UmamiScriptProps) {
  if (!websiteId) return null

  return (
    <Script
      defer
      src={`${host}/script.js`}
      data-website-id={websiteId}
      data-host-url={host}
      strategy="afterInteractive"
    />
  )
}

2. Layout Integration (Multi-Tenant)

// src/app/layout.tsx

import { UmamiScript } from '@/components/analytics/UmamiScript'
import { UMAMI_WEBSITE_IDS, UMAMI_HOST } from '@/config/analytics'
import { getCurrentTenant } from '@/lib/tenant'

export default async function RootLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  const tenant = await getCurrentTenant()
  const umamiWebsiteId = UMAMI_WEBSITE_IDS[tenant.domain]

  return (
    <html lang="de">
      <body>
        {children}
        
        {/* Umami Analytics - Läuft OHNE Consent (cookieless) */}
        {umamiWebsiteId && (
          <UmamiScript 
            websiteId={umamiWebsiteId} 
            host={UMAMI_HOST}
          />
        )}
      </body>
    </html>
  )
}

3. Analytics Hook für Custom Events

// src/hooks/useAnalytics.ts

'use client'

import { useCallback } from 'react'

declare global {
  interface Window {
    umami?: {
      track: (eventName: string, eventData?: Record<string, unknown>) => void
    }
    gtag?: (...args: unknown[]) => void
    CookieConsent?: {
      acceptedCategory: (category: string) => boolean
    }
  }
}

export function useAnalytics() {
  /**
   * Generisches Event-Tracking
   */
  const trackEvent = useCallback((
    eventName: string, 
    eventData?: Record<string, unknown>
  ) => {
    if (typeof window !== 'undefined' && window.umami) {
      window.umami.track(eventName, eventData)
    }
  }, [])

  /**
   * Newsletter Anmeldung
   */
  const trackNewsletterSubscribe = useCallback((source: string = 'unknown') => {
    trackEvent('newsletter_subscribe', { source })
  }, [trackEvent])

  /**
   * Newsletter Bestätigung (Double Opt-In)
   */
  const trackNewsletterConfirm = useCallback(() => {
    trackEvent('newsletter_confirm')
  }, [trackEvent])

  /**
   * Kontaktformular abgesendet
   */
  const trackContactFormSubmit = useCallback((formType: string = 'contact') => {
    trackEvent('contact_form_submit', { form_type: formType })
  }, [trackEvent])

  /**
   * CTA Klick
   */
  const trackCtaClick = useCallback((
    ctaName: string, 
    ctaLocation: string = 'unknown'
  ) => {
    trackEvent('cta_click', { cta_name: ctaName, location: ctaLocation })
  }, [trackEvent])

  /**
   * Download
   */
  const trackDownload = useCallback((
    fileName: string, 
    fileType: string = 'unknown'
  ) => {
    trackEvent('download', { file_name: fileName, file_type: fileType })
  }, [trackEvent])

  /**
   * Funnel-Step (für Conversion-Funnels)
   */
  const trackFunnelStep = useCallback((
    funnelName: string, 
    stepNumber: number, 
    stepName: string
  ) => {
    trackEvent('funnel_step', { 
      funnel: funnelName, 
      step: stepNumber, 
      step_name: stepName 
    })
  }, [trackEvent])

  /**
   * Scroll-Tiefe
   */
  const trackScrollDepth = useCallback((depth: number) => {
    trackEvent('scroll_depth', { depth_percent: depth })
  }, [trackEvent])

  /**
   * Externer Link Klick
   */
  const trackExternalLink = useCallback((url: string) => {
    trackEvent('external_link', { url })
  }, [trackEvent])

  return {
    trackEvent,
    trackNewsletterSubscribe,
    trackNewsletterConfirm,
    trackContactFormSubmit,
    trackCtaClick,
    trackDownload,
    trackFunnelStep,
    trackScrollDepth,
    trackExternalLink,
  }
}

4. Server-Side Event Tracking

// src/lib/analytics.server.ts

const UMAMI_HOST = process.env.UMAMI_HOST || 'https://analytics.c2sgmbh.de'
const UMAMI_WEBSITE_ID = process.env.UMAMI_WEBSITE_ID

interface ServerEventParams {
  event: string
  url: string
  websiteId?: string
  referrer?: string
  data?: Record<string, unknown>
}

/**
 * Server-Side Event an Umami senden
 * Für Events die im Backend passieren (z.B. Newsletter-Bestätigung)
 */
export async function trackServerEvent(params: ServerEventParams) {
  const websiteId = params.websiteId || UMAMI_WEBSITE_ID
  
  if (!websiteId) {
    console.warn('[Analytics] No websiteId configured for server-side tracking')
    return
  }

  try {
    const response = await fetch(`${UMAMI_HOST}/api/send`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'Payload-CMS-Server/1.0',
      },
      body: JSON.stringify({
        type: 'event',
        payload: {
          website: websiteId,
          url: params.url,
          referrer: params.referrer || '',
          name: params.event,
          data: params.data,
        },
      }),
    })

    if (!response.ok) {
      console.error('[Analytics] Server event failed:', response.status)
    }
  } catch (error) {
    console.error('[Analytics] Server event error:', error)
  }
}

Komponenten-Beispiele

Newsletter-Formular mit Tracking

// src/components/forms/NewsletterForm.tsx

'use client'

import { useState } from 'react'
import { useAnalytics } from '@/hooks/useAnalytics'

interface NewsletterFormProps {
  source?: string  // z.B. 'footer', 'hero', 'popup'
}

export function NewsletterForm({ source = 'unknown' }: NewsletterFormProps) {
  const [email, setEmail] = useState('')
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
  const { trackNewsletterSubscribe, trackFunnelStep } = useAnalytics()

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setStatus('loading')

    // Funnel-Step tracken
    trackFunnelStep('newsletter', 1, 'form_submitted')

    try {
      const response = await fetch('/api/newsletter/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, source }),
      })

      if (response.ok) {
        setStatus('success')
        // Erfolg tracken
        trackNewsletterSubscribe(source)
        trackFunnelStep('newsletter', 2, 'subscription_pending')
      } else {
        setStatus('error')
      }
    } catch {
      setStatus('error')
    }
  }

  if (status === 'success') {
    return (
      <div className="p-4 bg-green-50 text-green-800 rounded">
        Bitte bestätige deine E-Mail-Adresse.
      </div>
    )
  }

  return (
    <form onSubmit={handleSubmit} className="flex gap-2">
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="E-Mail-Adresse"
        required
        className="flex-1 px-4 py-2 border rounded"
      />
      <button
        type="submit"
        disabled={status === 'loading'}
        className="px-6 py-2 bg-primary text-white rounded"
      >
        {status === 'loading' ? 'Lädt...' : 'Anmelden'}
      </button>
    </form>
  )
}

CTA Button mit Tracking

// src/components/ui/TrackedButton.tsx

'use client'

import { useAnalytics } from '@/hooks/useAnalytics'
import Link from 'next/link'

interface TrackedButtonProps {
  href: string
  ctaName: string
  location?: string
  children: React.ReactNode
  className?: string
  external?: boolean
}

export function TrackedButton({
  href,
  ctaName,
  location = 'unknown',
  children,
  className,
  external = false,
}: TrackedButtonProps) {
  const { trackCtaClick, trackExternalLink } = useAnalytics()

  function handleClick() {
    trackCtaClick(ctaName, location)
    if (external) {
      trackExternalLink(href)
    }
  }

  if (external) {
    return (
      <a
        href={href}
        target="_blank"
        rel="noopener noreferrer"
        onClick={handleClick}
        className={className}
      >
        {children}
      </a>
    )
  }

  return (
    <Link href={href} onClick={handleClick} className={className}>
      {children}
    </Link>
  )
}
// src/components/ui/TrackedDownload.tsx

'use client'

import { useAnalytics } from '@/hooks/useAnalytics'

interface TrackedDownloadProps {
  href: string
  fileName: string
  fileType?: string
  children: React.ReactNode
  className?: string
}

export function TrackedDownload({
  href,
  fileName,
  fileType = 'document',
  children,
  className,
}: TrackedDownloadProps) {
  const { trackDownload } = useAnalytics()

  function handleClick() {
    trackDownload(fileName, fileType)
  }

  return (
    <a
      href={href}
      download
      onClick={handleClick}
      className={className}
    >
      {children}
    </a>
  )
}

Google Ads Integration

// src/components/analytics/GoogleConsentMode.tsx

'use client'

import Script from 'next/script'
import { useEffect } from 'react'

interface GoogleConsentModeProps {
  googleAdsId: string
}

export function GoogleConsentMode({ googleAdsId }: GoogleConsentModeProps) {
  useEffect(() => {
    // Consent-Änderungen von Orestbida abonnieren
    window.addEventListener('cc:onConsent', handleConsentChange)
    window.addEventListener('cc:onChange', handleConsentChange)
    
    // Initial setzen
    updateGoogleConsent()
    
    return () => {
      window.removeEventListener('cc:onConsent', handleConsentChange)
      window.removeEventListener('cc:onChange', handleConsentChange)
    }
  }, [])

  function handleConsentChange() {
    updateGoogleConsent()
  }

  function updateGoogleConsent() {
    if (typeof window.gtag !== 'function') return
    
    const cc = window.CookieConsent
    if (!cc) return

    const hasMarketing = cc.acceptedCategory('marketing')

    window.gtag('consent', 'update', {
      'ad_storage': hasMarketing ? 'granted' : 'denied',
      'ad_user_data': hasMarketing ? 'granted' : 'denied',
      'ad_personalization': hasMarketing ? 'granted' : 'denied',
      'analytics_storage': 'denied', // Wir nutzen Umami
    })
  }

  return (
    <>
      {/* Consent Default (vor gtag.js) */}
      <Script
        id="google-consent-default"
        strategy="beforeInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            
            gtag('consent', 'default', {
              'ad_storage': 'denied',
              'ad_user_data': 'denied',
              'ad_personalization': 'denied',
              'analytics_storage': 'denied',
              'wait_for_update': 500
            });
          `,
        }}
      />

      {/* Google Ads Tag */}
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${googleAdsId}`}
        strategy="afterInteractive"
      />
      
      <Script
        id="google-ads-config"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            gtag('js', new Date());
            gtag('config', '${googleAdsId}', {
              'allow_enhanced_conversions': true
            });
          `,
        }}
      />
    </>
  )
}

GCLID Hook (für Conversion Attribution)

// src/hooks/useGclid.ts

'use client'

import { useEffect } from 'react'
import { useSearchParams } from 'next/navigation'

const GCLID_STORAGE_KEY = 'gclid'
const GCLID_EXPIRY_DAYS = 90

/**
 * GCLID aus URL erfassen und speichern
 */
export function useGclid() {
  const searchParams = useSearchParams()

  useEffect(() => {
    const gclid = searchParams.get('gclid')
    
    if (gclid) {
      const data = {
        value: gclid,
        expires: Date.now() + (GCLID_EXPIRY_DAYS * 24 * 60 * 60 * 1000)
      }
      localStorage.setItem(GCLID_STORAGE_KEY, JSON.stringify(data))
    }
  }, [searchParams])
}

/**
 * Gespeicherte GCLID abrufen
 */
export function getStoredGclid(): string | null {
  if (typeof window === 'undefined') return null
  
  try {
    const stored = localStorage.getItem(GCLID_STORAGE_KEY)
    if (!stored) return null
    
    const data = JSON.parse(stored)
    
    if (Date.now() > data.expires) {
      localStorage.removeItem(GCLID_STORAGE_KEY)
      return null
    }
    
    return data.value
  } catch {
    return null
  }
}

Client-Side Conversion Tracking

// src/lib/google-ads.ts

declare global {
  interface Window {
    gtag: (...args: unknown[]) => void
    CookieConsent: {
      acceptedCategory: (category: string) => boolean
    }
  }
}

interface ConversionParams {
  conversionId: string
  value?: number
  currency?: string
  transactionId?: string
}

/**
 * Client-Side Conversion (nur bei Marketing-Consent)
 */
export function trackConversion(params: ConversionParams) {
  if (typeof window === 'undefined') return
  if (typeof window.gtag !== 'function') return
  
  const hasConsent = window.CookieConsent?.acceptedCategory('marketing')
  if (!hasConsent) return

  window.gtag('event', 'conversion', {
    'send_to': params.conversionId,
    'value': params.value || 1.0,
    'currency': params.currency || 'EUR',
    'transaction_id': params.transactionId,
  })
}

/**
 * Enhanced Conversion Data setzen
 */
export function setEnhancedConversionData(data: {
  email?: string
  phone?: string
  firstName?: string
  lastName?: string
}) {
  if (typeof window === 'undefined') return
  if (typeof window.gtag !== 'function') return
  
  const hasConsent = window.CookieConsent?.acceptedCategory('marketing')
  if (!hasConsent) return

  window.gtag('set', 'user_data', {
    'email': data.email,
    'phone_number': data.phone,
    'address': {
      'first_name': data.firstName,
      'last_name': data.lastName,
    }
  })
}

Server-Side Conversion API

// src/lib/google-ads.server.ts

import crypto from 'crypto'

const GOOGLE_ADS_CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID
const GOOGLE_ADS_CONVERSION_ACTION_ID = process.env.GOOGLE_ADS_CONVERSION_ACTION_ID
const GOOGLE_ADS_API_TOKEN = process.env.GOOGLE_ADS_API_TOKEN
const GOOGLE_ADS_DEVELOPER_TOKEN = process.env.GOOGLE_ADS_DEVELOPER_TOKEN

interface ServerConversionParams {
  conversionAction: string
  email?: string
  phone?: string
  value?: number
  currency?: string
  gclid?: string
}

/**
 * Server-Side Conversion Upload
 * DSGVO-konform: Nur gehashte Daten
 */
export async function trackServerConversion(params: ServerConversionParams) {
  if (!GOOGLE_ADS_CUSTOMER_ID || !GOOGLE_ADS_API_TOKEN) {
    console.log('[Google Ads] Server-Side nicht konfiguriert')
    return
  }

  const conversion: Record<string, unknown> = {
    conversionAction: `customers/${GOOGLE_ADS_CUSTOMER_ID}/conversionActions/${GOOGLE_ADS_CONVERSION_ACTION_ID}`,
    conversionDateTime: new Date().toISOString().replace('Z', '+00:00'),
    conversionValue: params.value || 1.0,
    currencyCode: params.currency || 'EUR',
  }

  if (params.gclid) {
    conversion.gclid = params.gclid
  }

  if (params.email || params.phone) {
    conversion.userIdentifiers = []
    
    if (params.email) {
      (conversion.userIdentifiers as Array<unknown>).push({
        hashedEmail: hashForGoogle(params.email.toLowerCase().trim())
      })
    }
    
    if (params.phone) {
      (conversion.userIdentifiers as Array<unknown>).push({
        hashedPhoneNumber: hashForGoogle(params.phone.replace(/[^0-9+]/g, ''))
      })
    }
  }

  try {
    const response = await fetch(
      `https://googleads.googleapis.com/v15/customers/${GOOGLE_ADS_CUSTOMER_ID}:uploadConversionAdjustments`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${GOOGLE_ADS_API_TOKEN}`,
          'Content-Type': 'application/json',
          'developer-token': GOOGLE_ADS_DEVELOPER_TOKEN!,
        },
        body: JSON.stringify({
          conversions: [conversion],
          partialFailure: true,
        }),
      }
    )

    if (!response.ok) {
      console.error('[Google Ads] Upload failed:', await response.text())
    }
  } catch (error) {
    console.error('[Google Ads] Error:', error)
  }
}

function hashForGoogle(value: string): string {
  return crypto.createHash('sha256').update(value).digest('hex')
}

CookieInventory Erweiterung

Füge diese Cookies zur cookie-inventory Collection hinzu:

// Über Payload Admin oder Seed-Script

const googleAdsCookies = [
  {
    name: '_gcl_au',
    provider: 'Google Ads',
    category: 'marketing',
    purpose: 'Conversion-Tracking und Anzeigenmessung',
    duration: '90 Tage',
    type: 'first-party',
  },
  {
    name: '_gcl_aw',
    provider: 'Google Ads',
    category: 'marketing',
    purpose: 'Speichert GCLID für Conversion-Attribution',
    duration: '90 Tage',
    type: 'first-party',
  },
  {
    name: 'IDE',
    provider: 'Google (doubleclick.net)',
    category: 'marketing',
    purpose: 'Remarketing und Anzeigenpersonalisierung',
    duration: '1 Jahr',
    type: 'third-party',
  },
]

Environment Variables

Frontend (.env.local)

# Payload CMS API (Production)
NEXT_PUBLIC_PAYLOAD_URL=https://cms.c2sgmbh.de
NEXT_PUBLIC_API_URL=https://cms.c2sgmbh.de/api

# Umami Analytics (Production)
NEXT_PUBLIC_UMAMI_HOST=https://analytics.c2sgmbh.de

# Tenant-Konfiguration (je nach Projekt)
NEXT_PUBLIC_TENANT_ID=4
NEXT_PUBLIC_TENANT_SLUG=c2s

# Google Ads (pro Tenant unterschiedlich)
NEXT_PUBLIC_GOOGLE_ADS_ID=AW-XXXXXXXXX

Backend (.env)

# Umami Server-Side Tracking
UMAMI_HOST=https://analytics.c2sgmbh.de
UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# Google Ads Server-Side API
GOOGLE_ADS_CUSTOMER_ID=1234567890
GOOGLE_ADS_CONVERSION_ACTION_ID=987654321
GOOGLE_ADS_API_TOKEN=ya29.xxx
GOOGLE_ADS_DEVELOPER_TOKEN=xxx

Event-Naming-Konvention

Event Name Data
Newsletter Anmeldung newsletter_subscribe { source: string }
Newsletter Bestätigung newsletter_confirm -
Kontaktformular contact_form_submit { form_type: string }
CTA Klick cta_click { cta_name: string, location: string }
Download download { file_name: string, file_type: string }
Funnel-Step funnel_step { funnel: string, step: number, step_name: string }
Scroll-Tiefe scroll_depth { depth_percent: number }
Externer Link external_link { url: string }

Zusammenfassung

┌─────────────────────────────────────────────────────────────────────────────────────┐
│  WAS IMPLEMENTIERT WERDEN MUSS                                                      │
│                                                                                     │
│  1. UMAMI (ohne Consent)                                                            │
│  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  │
│  • UmamiScript Komponente in Layout einbinden                                       │
│  • useAnalytics Hook in Formulare/CTAs integrieren                                 │
│  • Server-Side Events für Backend-Actions                                          │
│                                                                                     │
│  2. GOOGLE ADS (mit Consent)                                                        │
│  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  │
│  • GoogleConsentMode Komponente (integriert mit Orestbida)                         │
│  • useGclid Hook für Attribution                                                    │
│  • Client-Side Conversions bei Consent                                              │
│  • Server-Side Conversions immer (gehashte Daten)                                  │
│                                                                                     │
│  3. COOKIE INVENTORY                                                                │
│  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  │
│  • Google Ads Cookies dokumentieren                                                 │
│                                                                                     │
└─────────────────────────────────────────────────────────────────────────────────────┘

Dateien zu erstellen

src/
├── components/
│   ├── analytics/
│   │   ├── UmamiScript.tsx           # Umami Tracking Script
│   │   └── GoogleConsentMode.tsx     # Google Consent Mode v2
│   ├── forms/
│   │   └── NewsletterForm.tsx        # Mit Analytics-Integration
│   └── ui/
│       ├── TrackedButton.tsx         # CTA mit Tracking
│       └── TrackedDownload.tsx       # Download mit Tracking
├── config/
│   └── analytics.ts                  # Website-IDs, Config
├── hooks/
│   ├── useAnalytics.ts               # Client-Side Event Tracking
│   └── useGclid.ts                   # GCLID Erfassung
└── lib/
    ├── analytics.server.ts           # Umami Server-Side
    ├── google-ads.ts                 # Google Ads Client
    └── google-ads.server.ts          # Google Ads Server API

Letzte Aktualisierung: 18. Dezember 2025