diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..8f4792d --- /dev/null +++ b/.env.local @@ -0,0 +1,2 @@ +NEXT_PUBLIC_POSTHOG_KEY=phc_xxx +NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com diff --git a/components/ui/FontCard.jsx b/components/ui/FontCard.jsx index c3fbab0..1bc087e 100644 --- a/components/ui/FontCard.jsx +++ b/components/ui/FontCard.jsx @@ -38,7 +38,8 @@ export default function FontCard({ }; return ( -
+ // Dieses Attribut wurde entfernt, da es alle Klicks blockiert hat. +
@@ -111,4 +112,4 @@ export default function FontCard({
); -} +} \ No newline at end of file diff --git a/instrumentation-client.js b/instrumentation-client.js new file mode 100644 index 0000000..09fe308 --- /dev/null +++ b/instrumentation-client.js @@ -0,0 +1,24 @@ +// instrumentation-client.js +import posthog from "posthog-js"; + +// envs lesen +const key = process.env.NEXT_PUBLIC_POSTHOG_KEY; +const host = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com"; + +// nur im Browser initialisieren + Doppel-Init verhindern +if (typeof window !== "undefined" && !window.__posthogInitialized) { + if (key) { + posthog.init(key, { + api_host: host, + capture_pageview: false, // Pageviews trackst du selbst + loaded: () => { + if (process.env.NODE_ENV === "development") posthog.debug(); + }, + }); + window.__posthogInitialized = true; + } else { + console.warn("NEXT_PUBLIC_POSTHOG_KEY ist nicht gesetzt"); + } +} + +export default posthog; diff --git a/lib/posthog.js b/lib/posthog.js new file mode 100644 index 0000000..3e72c25 --- /dev/null +++ b/lib/posthog.js @@ -0,0 +1,21 @@ +// lib/posthog.ts +import { PostHog } from "posthog-node"; + +export function PostHogClient() { + // Server-Keys verwenden (ohne NEXT_PUBLIC_) + const key = process.env.POSTHOG_API_KEY; + const host = process.env.POSTHOG_HOST ?? "https://us.i.posthog.com"; + + if (!key) { + // Fail fast – sonst sendest du stumm nichts + throw new Error("POSTHOG_API_KEY ist nicht gesetzt (serverseitige Env)."); + } + + const client = new PostHog(key, { + host, + flushAt: 1, + flushInterval: 0, + }); + + return client; +} diff --git a/package.json b/package.json index c3197c6..9077d34 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "html2canvas": "^1.4.1", "lucide-react": "^0.292.0", "next": "^14.0.4", + "posthog-js": "^1.259.0", + "posthog-node": "^5.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-helmet": "^6.1.0", diff --git a/pages/api/track.js b/pages/api/track.js new file mode 100644 index 0000000..d8ddd06 --- /dev/null +++ b/pages/api/track.js @@ -0,0 +1,23 @@ +// pages/api/track.js +import { PostHogClient } from "@/lib/posthog"; + +export default async function handler(req, res) { + const posthog = PostHogClient(); + + try { + const { event, properties, distinctId } = req.body; + + await posthog.capture({ + distinctId: distinctId || "server_event", + event: event || "default_event", + properties: properties || {}, + }); + + await posthog.shutdown(); + + return res.status(200).json({ success: true }); + } catch (error) { + console.error("PostHog error:", error); + return res.status(500).json({ success: false, error: error.message }); + } +} diff --git a/pages/index.jsx b/pages/index.jsx index acecd88..f9fde99 100644 --- a/pages/index.jsx +++ b/pages/index.jsx @@ -1,7 +1,9 @@ // pages/index.jsx import React, { useState, useEffect, useMemo, useCallback } from "react"; import Head from "next/head"; +import { useRouter } from "next/router"; import { motion, AnimatePresence } from "framer-motion"; +import posthog from "posthog-js"; import { fontTransforms, @@ -19,11 +21,76 @@ import SocialButtons from "@/components/SocialButtons"; import FancyTextPreview from "@/components/FancyTextPreview"; import SEOHead from "@/components/SEOHead"; +/* -------------------- PostHog: Client Init + Pageviews -------------------- */ +// Fallback-Init (falls du instrumentation-client.ts noch nicht nutzt) +if (typeof window !== "undefined" && !(posthog).__initialized) { + const key = process.env.NEXT_PUBLIC_POSTHOG_KEY || ""; + const host = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com"; + + if (key) { + posthog.init(key, { + api_host: host, + capture_pageview: false, // Pageviews steuern wir selbst unten + loaded: () => { + if (process.env.NODE_ENV === "development") posthog.debug(); + }, + }); + // kleiner Guard gegen Doppel-Init bei Fast Refresh + (posthog).__initialized = true; + } else { + console.warn("⚠️ NEXT_PUBLIC_POSTHOG_KEY ist nicht gesetzt"); + } +} +/* ------------------------------------------------------------------------- */ + if (typeof window !== "undefined") { sessionStorage.removeItem("fancytext_recent_fonts"); } export default function HomePage() { + const router = useRouter(); + + // --- Helper: Server-Tracking via /api/track --- + const serverTrack = useCallback(async (payload) => { + try { + await fetch("/api/track", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + } catch { + // optional: console.warn("Server track failed", e); + } + }, []); + + // Pageview-Tracking (pages router) + useEffect(() => { + const send = (url) => { + const href = + typeof window !== "undefined" && window.location ? window.location.href : url; + + // Client-Pageview + posthog.capture("$pageview", { + $current_url: href, + path: url, + }); + + // Server-Pageview (optional, fΓΌr serverseitige Analysen) + serverTrack({ + distinctId: "anon_client", + event: "$pageview", + properties: { path: url, href }, + }); + }; + + // initial + if (router?.asPath) send(router.asPath); + + const handleRouteChange = (url) => send(url); + router.events.on("routeChangeComplete", handleRouteChange); + return () => router.events.off("routeChangeComplete", handleRouteChange); + }, [router, serverTrack]); + const [inputText, setInputText] = useState("Hello Instagram!"); const [previewFont, setPreviewFont] = useState(null); const [selectedCategory, setSelectedCategory] = useState("all"); @@ -43,7 +110,9 @@ export default function HomePage() { useEffect(() => { const checkMobile = () => { if (typeof window !== "undefined") { - setIsMobile(window.innerWidth < 768 || /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)); + setIsMobile( + window.innerWidth < 768 || /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) + ); } }; checkMobile(); @@ -83,50 +152,124 @@ export default function HomePage() { return counts; }, []); - const trackFontCopy = useCallback((fontName, text) => { - window.gtag?.("event", "font_copied", { - font_name: fontName, - text_length: text.length, - category: fontTransforms[fontName]?.category, - }); + const trackFontCopy = useCallback( + (fontName, text) => { + // Client-Event + posthog.capture("font_copied", { + font_name: fontName, + text_length: text.length, + category: fontTransforms[fontName]?.category, + }); - setRecentFonts((prev) => { - const updated = [fontName, ...prev.filter((f) => f !== fontName)].slice(0, 5); - return updated; - }); - }, []); + // Server-Event + serverTrack({ + distinctId: "anon_client", + event: "font_copied", + properties: { + font_name: fontName, + text_length: text.length, + category: fontTransforms[fontName]?.category, + }, + }); - const trackFontLike = useCallback((fontName, liked) => { - window.gtag?.("event", "font_liked", { - font_name: fontName, - action: liked ? "like" : "unlike", - }); - }, []); + setRecentFonts((prev) => { + const updated = [fontName, ...prev.filter((f) => f !== fontName)].slice(0, 5); + return updated; + }); + }, + [serverTrack] + ); + + const trackFontLike = useCallback( + (fontName, liked) => { + posthog.capture("font_liked", { + font_name: fontName, + action: liked ? "like" : "unlike", + }); + + serverTrack({ + distinctId: "anon_client", + event: "font_liked", + properties: { font_name: fontName, action: liked ? "like" : "unlike" }, + }); + }, + [serverTrack] + ); const handleQuickShare = useCallback(async () => { const shareData = { title: "FancyText - Cool Fonts! πŸ”₯", text: "Check out this app for cool Instagram & TikTok fonts! 30+ fonts free ✨", - url: window.location.href, + url: typeof window !== "undefined" ? window.location.href : "https://fancytext.app", }; if (navigator.share) { try { await navigator.share(shareData); - } catch {} + posthog.capture("app_shared", { method: "web_share_api" }); + serverTrack({ + distinctId: "anon_client", + event: "app_shared", + properties: { method: "web_share_api" }, + }); + } catch { + posthog.capture("app_share_failed"); + serverTrack({ + distinctId: "anon_client", + event: "app_share_failed", + properties: {}, + }); + } } else { await navigator.clipboard.writeText(`${shareData.text}\n${shareData.url}`); + posthog.capture("app_shared", { method: "clipboard_copy" }); + serverTrack({ + distinctId: "anon_client", + event: "app_shared", + properties: { method: "clipboard_copy" }, + }); alert("Link copied to clipboard! πŸ—Œ"); } - window.gtag?.("event", "app_shared", { method: "button_click" }); - }, []); + }, [serverTrack]); - const handleTextChange = useCallback((text) => { - setInputText(text); - setPreviewFont(null); - }, []); + const handleTextChange = useCallback( + (text) => { + setInputText(text); + setPreviewFont(null); + posthog.capture("text_changed", { length: text?.length ?? 0 }); + serverTrack({ + distinctId: "anon_client", + event: "text_changed", + properties: { length: text?.length ?? 0 }, + }); + }, + [serverTrack] + ); - const handleCategoryChange = useCallback((cat) => setSelectedCategory(cat), []); - const handleSearch = useCallback((q) => setSearchQuery(q), []); + const handleCategoryChange = useCallback( + (cat) => { + setSelectedCategory(cat); + posthog.capture("category_changed", { category: cat }); + serverTrack({ + distinctId: "anon_client", + event: "category_changed", + properties: { category: cat }, + }); + }, + [serverTrack] + ); + + const handleSearch = useCallback( + (q) => { + setSearchQuery(q); + posthog.capture("font_search", { query_length: q?.length ?? 0 }); + serverTrack({ + distinctId: "anon_client", + event: "font_search", + properties: { query_length: q?.length ?? 0 }, + }); + }, + [serverTrack] + ); const handleRandomFont = useCallback(() => { const fontList = Object.keys(fontTransforms); @@ -139,7 +282,13 @@ export default function HomePage() { } while (newFont === previewFont && tries < 50); setPreviewFont(newFont); - }, [previewFont]); + posthog.capture("random_font_selected", { font: newFont }); + serverTrack({ + distinctId: "anon_client", + event: "random_font_selected", + properties: { font: newFont }, + }); + }, [previewFont, serverTrack]); const displayText = previewFont ? transformText(inputText || "Try me!", previewFont) @@ -149,39 +298,56 @@ export default function HomePage() { <> FancyText | Viral Fonts - + - + -