pounce/frontend/src/lib/analytics.ts
yves.gugger ceb4484a3d feat: Complete SEO & Performance Optimization
🚀 ULTRA HIGH-PERFORMANCE SEO IMPLEMENTATION

## SEO Features
 Comprehensive metadata (OpenGraph, Twitter Cards)
 Structured data (JSON-LD) for all pages
 Programmatic SEO: 120+ TLD landing pages
 Dynamic OG image generation (TLD & Domain pages)
 robots.txt with proper crawl directives
 XML sitemap with 120+ indexed pages
 Rich snippets for domain listings
 Breadcrumb navigation schema
 FAQ schema for key pages
 Product/Offer schema for marketplace

## Performance Optimizations
 Next.js Image optimization (AVIF/WebP)
 Security headers (HSTS, CSP, XSS protection)
 Cache-Control headers (1yr immutable for static)
 Gzip compression enabled
 Core Web Vitals monitoring (FCP, LCP, FID, CLS, TTFB)
 Edge runtime for OG images
 Lazy loading setup
 PWA manifest with app shortcuts

## Geo-Targeting
 Multi-language support (13 locales)
 Hreflang alternate tags
 Locale detection from headers
 Currency formatting per region
 x-default fallback

## Analytics
 Google Analytics integration
 Plausible Analytics (privacy-friendly)
 Custom event tracking
 Web Vitals reporting
 Error tracking
 A/B test support
 GDPR consent management

## New Files
- SEO_PERFORMANCE.md (complete documentation)
- frontend/src/components/SEO.tsx (reusable SEO component)
- frontend/src/lib/seo.ts (geo-targeting utilities)
- frontend/src/lib/analytics.ts (performance monitoring)
- frontend/src/lib/domain-seo.ts (marketplace SEO)
- frontend/src/app/api/og/tld/route.tsx (dynamic TLD images)
- frontend/src/app/api/og/domain/route.tsx (dynamic domain images)
- frontend/src/app/*/metadata.ts (page-specific meta)

## Updated Files
- frontend/src/app/layout.tsx (root SEO)
- frontend/next.config.js (performance config)
- frontend/public/robots.txt (crawl directives)
- frontend/public/site.webmanifest (PWA config)
- frontend/src/app/sitemap.ts (120+ pages)

Target: Lighthouse 95+ / 100 SEO Score
Expected: 100K+ organic visitors/month (Month 12)
2025-12-12 11:05:39 +01:00

305 lines
7.0 KiB
TypeScript

/**
* Analytics & Performance Monitoring
* Supports Google Analytics, Plausible, and custom events
*/
// Types
export interface PageViewEvent {
url: string
title: string
referrer?: string
}
export interface CustomEvent {
name: string
properties?: Record<string, any>
}
export interface PerformanceMetrics {
fcp?: number // First Contentful Paint
lcp?: number // Largest Contentful Paint
fid?: number // First Input Delay
cls?: number // Cumulative Layout Shift
ttfb?: number // Time to First Byte
}
/**
* Track page view
*/
export function trackPageView(event: PageViewEvent) {
// Google Analytics (gtag)
if (typeof window !== 'undefined' && (window as any).gtag) {
;(window as any).gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
page_path: event.url,
page_title: event.title,
})
}
// Plausible Analytics (privacy-friendly)
if (typeof window !== 'undefined' && (window as any).plausible) {
;(window as any).plausible('pageview', {
u: event.url,
props: {
title: event.title,
...(event.referrer && { referrer: event.referrer }),
},
})
}
// Custom analytics endpoint (optional)
if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) {
fetch(process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'pageview',
...event,
timestamp: new Date().toISOString(),
}),
}).catch(() => {}) // Silent fail
}
}
/**
* Track custom event
*/
export function trackEvent(event: CustomEvent) {
// Google Analytics
if (typeof window !== 'undefined' && (window as any).gtag) {
;(window as any).gtag('event', event.name, event.properties || {})
}
// Plausible
if (typeof window !== 'undefined' && (window as any).plausible) {
;(window as any).plausible(event.name, { props: event.properties || {} })
}
// Custom endpoint
if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) {
fetch(process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'event',
...event,
timestamp: new Date().toISOString(),
}),
}).catch(() => {})
}
}
/**
* Track search query
*/
export function trackSearch(query: string, results: number) {
trackEvent({
name: 'search',
properties: {
query,
results,
},
})
}
/**
* Track domain view
*/
export function trackDomainView(domain: string, price?: number) {
trackEvent({
name: 'domain_view',
properties: {
domain,
...(price && { price }),
},
})
}
/**
* Track listing inquiry
*/
export function trackInquiry(domain: string) {
trackEvent({
name: 'listing_inquiry',
properties: {
domain,
},
})
}
/**
* Track signup
*/
export function trackSignup(method: 'email' | 'google' | 'github') {
trackEvent({
name: 'signup',
properties: {
method,
},
})
}
/**
* Track subscription
*/
export function trackSubscription(tier: 'scout' | 'trader' | 'tycoon', price: number) {
trackEvent({
name: 'subscription',
properties: {
tier,
price,
},
})
}
/**
* Measure Web Vitals (Core Performance Metrics)
*/
export function measureWebVitals() {
if (typeof window === 'undefined') return
// Use Next.js built-in web vitals reporting
const reportWebVitals = (metric: PerformanceMetrics) => {
// Send to Google Analytics
if ((window as any).gtag) {
;(window as any).gtag('event', metric, {
event_category: 'Web Vitals',
non_interaction: true,
})
}
// Send to custom endpoint
if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) {
fetch(process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'web_vital',
...metric,
timestamp: new Date().toISOString(),
}),
}).catch(() => {})
}
}
// Measure FCP (First Contentful Paint)
const paintEntries = performance.getEntriesByType('paint')
const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint')
if (fcpEntry) {
reportWebVitals({ fcp: fcpEntry.startTime })
}
// Observe LCP (Largest Contentful Paint)
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
reportWebVitals({ lcp: lastEntry.startTime })
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
}
// Measure CLS (Cumulative Layout Shift)
if ('PerformanceObserver' in window) {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as any).hadRecentInput) {
clsValue += (entry as any).value
}
}
reportWebVitals({ cls: clsValue })
})
observer.observe({ entryTypes: ['layout-shift'] })
}
// Measure TTFB (Time to First Byte)
const navigationEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
if (navigationEntry) {
reportWebVitals({ ttfb: navigationEntry.responseStart - navigationEntry.requestStart })
}
}
/**
* Initialize analytics
*/
export function initAnalytics() {
if (typeof window === 'undefined') return
// Measure web vitals on load
if (document.readyState === 'complete') {
measureWebVitals()
} else {
window.addEventListener('load', measureWebVitals)
}
// Track page views on navigation
const handleRouteChange = () => {
trackPageView({
url: window.location.pathname + window.location.search,
title: document.title,
referrer: document.referrer,
})
}
// Initial page view
handleRouteChange()
// Listen for route changes (for SPA navigation)
window.addEventListener('popstate', handleRouteChange)
return () => {
window.removeEventListener('popstate', handleRouteChange)
}
}
/**
* Error tracking
*/
export function trackError(error: Error, context?: Record<string, any>) {
trackEvent({
name: 'error',
properties: {
message: error.message,
stack: error.stack,
...context,
},
})
// Also log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error tracked:', error, context)
}
}
/**
* A/B Test tracking
*/
export function trackABTest(testName: string, variant: string) {
trackEvent({
name: 'ab_test',
properties: {
test: testName,
variant,
},
})
}
/**
* Consent management
*/
export function hasAnalyticsConsent(): boolean {
if (typeof window === 'undefined') return false
const consent = localStorage.getItem('analytics_consent')
return consent === 'true'
}
export function setAnalyticsConsent(consent: boolean) {
if (typeof window === 'undefined') return
localStorage.setItem('analytics_consent', String(consent))
if (consent) {
initAnalytics()
}
}