Posthog integration

This commit is contained in:
Timo Knuth 2025-08-11 08:54:45 +02:00
parent 878dbc63f7
commit 1179b406b8
7 changed files with 322 additions and 76 deletions

2
.env.local Normal file
View File

@ -0,0 +1,2 @@
NEXT_PUBLIC_POSTHOG_KEY=phc_xxx
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@ -38,7 +38,8 @@ export default function FontCard({
}; };
return ( return (
<div style={{ pointerEvents: "none" }}> // Dieses Attribut wurde entfernt, da es alle Klicks blockiert hat.
<div>
<Card className="bg-white/90 backdrop-blur-sm border-0 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden"> <Card className="bg-white/90 backdrop-blur-sm border-0 shadow-xl hover:shadow-2xl transition-all duration-300 overflow-hidden">
<div className="p-6"> <div className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">

24
instrumentation-client.js Normal file
View File

@ -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;

21
lib/posthog.js Normal file
View File

@ -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;
}

View File

@ -20,6 +20,8 @@
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lucide-react": "^0.292.0", "lucide-react": "^0.292.0",
"next": "^14.0.4", "next": "^14.0.4",
"posthog-js": "^1.259.0",
"posthog-node": "^5.6.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",

23
pages/api/track.js Normal file
View File

@ -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 });
}
}

View File

@ -1,7 +1,9 @@
// pages/index.jsx // pages/index.jsx
import React, { useState, useEffect, useMemo, useCallback } from "react"; import React, { useState, useEffect, useMemo, useCallback } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import posthog from "posthog-js";
import { import {
fontTransforms, fontTransforms,
@ -19,11 +21,76 @@ import SocialButtons from "@/components/SocialButtons";
import FancyTextPreview from "@/components/FancyTextPreview"; import FancyTextPreview from "@/components/FancyTextPreview";
import SEOHead from "@/components/SEOHead"; 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") { if (typeof window !== "undefined") {
sessionStorage.removeItem("fancytext_recent_fonts"); sessionStorage.removeItem("fancytext_recent_fonts");
} }
export default function HomePage() { 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 [inputText, setInputText] = useState("Hello Instagram!");
const [previewFont, setPreviewFont] = useState(null); const [previewFont, setPreviewFont] = useState(null);
const [selectedCategory, setSelectedCategory] = useState("all"); const [selectedCategory, setSelectedCategory] = useState("all");
@ -43,7 +110,9 @@ export default function HomePage() {
useEffect(() => { useEffect(() => {
const checkMobile = () => { const checkMobile = () => {
if (typeof window !== "undefined") { 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(); checkMobile();
@ -83,50 +152,124 @@ export default function HomePage() {
return counts; return counts;
}, []); }, []);
const trackFontCopy = useCallback((fontName, text) => { const trackFontCopy = useCallback(
window.gtag?.("event", "font_copied", { (fontName, text) => {
// Client-Event
posthog.capture("font_copied", {
font_name: fontName, font_name: fontName,
text_length: text.length, text_length: text.length,
category: fontTransforms[fontName]?.category, category: fontTransforms[fontName]?.category,
}); });
// Server-Event
serverTrack({
distinctId: "anon_client",
event: "font_copied",
properties: {
font_name: fontName,
text_length: text.length,
category: fontTransforms[fontName]?.category,
},
});
setRecentFonts((prev) => { setRecentFonts((prev) => {
const updated = [fontName, ...prev.filter((f) => f !== fontName)].slice(0, 5); const updated = [fontName, ...prev.filter((f) => f !== fontName)].slice(0, 5);
return updated; return updated;
}); });
}, []); },
[serverTrack]
);
const trackFontLike = useCallback((fontName, liked) => { const trackFontLike = useCallback(
window.gtag?.("event", "font_liked", { (fontName, liked) => {
posthog.capture("font_liked", {
font_name: fontName, font_name: fontName,
action: liked ? "like" : "unlike", 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 handleQuickShare = useCallback(async () => {
const shareData = { const shareData = {
title: "FancyText - Cool Fonts! 🔥", title: "FancyText - Cool Fonts! 🔥",
text: "Check out this app for cool Instagram & TikTok fonts! 30+ fonts free ✨", 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) { if (navigator.share) {
try { try {
await navigator.share(shareData); 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 { } else {
await navigator.clipboard.writeText(`${shareData.text}\n${shareData.url}`); 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! 🗌"); alert("Link copied to clipboard! 🗌");
} }
window.gtag?.("event", "app_shared", { method: "button_click" }); }, [serverTrack]);
}, []);
const handleTextChange = useCallback((text) => { const handleTextChange = useCallback(
(text) => {
setInputText(text); setInputText(text);
setPreviewFont(null); 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 handleCategoryChange = useCallback(
const handleSearch = useCallback((q) => setSearchQuery(q), []); (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 handleRandomFont = useCallback(() => {
const fontList = Object.keys(fontTransforms); const fontList = Object.keys(fontTransforms);
@ -139,7 +282,13 @@ export default function HomePage() {
} while (newFont === previewFont && tries < 50); } while (newFont === previewFont && tries < 50);
setPreviewFont(newFont); 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 const displayText = previewFont
? transformText(inputText || "Try me!", previewFont) ? transformText(inputText || "Try me!", previewFont)
@ -149,17 +298,25 @@ export default function HomePage() {
<> <>
<Head> <Head>
<title>FancyText | Viral Fonts</title> <title>FancyText | Viral Fonts</title>
<meta name="description" content="Make your posts pop with 30+ copy-paste fonts. Free, no login, mobile-ready. Works on IG, TikTok, Threads & more." /> <meta
name="description"
content="Make your posts pop with 30+ copy-paste fonts. Free, no login, mobile-ready. Works on IG, TikTok, Threads & more."
/>
<link rel="canonical" href="https://fancytext.app" /> <link rel="canonical" href="https://fancytext.app" />
<meta property="og:title" content="30+ Fancy Fonts for TikTok & Instagram 🔥" /> <meta property="og:title" content="30+ Fancy Fonts for TikTok & Instagram 🔥" />
<meta property="og:description" content="Create viral bios, comments & posts in seconds no login, always free." /> <meta
property="og:description"
content="Create viral bios, comments & posts in seconds no login, always free."
/>
<meta property="og:image" content="https://fancytext.app/social-preview.png" /> <meta property="og:image" content="https://fancytext.app/social-preview.png" />
<meta property="og:url" content="https://fancytext.app" /> <meta property="og:url" content="https://fancytext.app" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<link rel="icon" href="/images/favicon.ico" /> <link rel="icon" href="/images/favicon.ico" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<script type="application/ld+json" dangerouslySetInnerHTML={{ <script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({ __html: JSON.stringify({
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebApplication", "@type": "WebApplication",
@ -172,16 +329,25 @@ export default function HomePage() {
price: "0.00", price: "0.00",
priceCurrency: "USD", priceCurrency: "USD",
}, },
}) }),
}} /> }}
/>
</Head> </Head>
<div className="fixed top-4 right-4 z-[100] flex gap-4 text-sm text-black bg-white/90 px-3 py-1 rounded-lg shadow-lg backdrop-blur-sm"> <div className="fixed top-4 right-4 z-[100] flex gap-4 text-sm text-black bg-white/90 px-3 py-1 rounded-lg shadow-lg backdrop-blur-sm">
<a href="#about" className="hover:underline">About</a> <a href="#about" className="hover:underline">
<a href="#" onClick={(e) => { About
</a>
<a
href="#"
onClick={(e) => {
e.preventDefault(); e.preventDefault();
document.getElementById("privacy")?.scrollIntoView({ behavior: "smooth" }); document.getElementById("privacy")?.scrollIntoView({ behavior: "smooth" });
}} className="hover:underline">Privacy</a> }}
className="hover:underline"
>
Privacy
</a>
</div> </div>
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800 relative overflow-hidden"> <div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800 relative overflow-hidden">
@ -189,7 +355,15 @@ export default function HomePage() {
<div className="relative z-10 container mx-auto px-4 py-8 max-w-6xl"> <div className="relative z-10 container mx-auto px-4 py-8 max-w-6xl">
<MobileOptimizedHeader <MobileOptimizedHeader
animationsEnabled={animationsEnabled} animationsEnabled={animationsEnabled}
onToggleAnimations={setAnimationsEnabled} onToggleAnimations={(val) => {
setAnimationsEnabled(val);
posthog.capture("animations_toggled", { enabled: val });
serverTrack({
distinctId: "anon_client",
event: "animations_toggled",
properties: { enabled: val },
});
}}
totalFonts={Object.keys(fontTransforms).length} totalFonts={Object.keys(fontTransforms).length}
onQuickShare={handleQuickShare} onQuickShare={handleQuickShare}
/> />
@ -254,7 +428,6 @@ export default function HomePage() {
</AnimatePresence> </AnimatePresence>
</motion.div> </motion.div>
<SocialButtons onShare={handleQuickShare} /> <SocialButtons onShare={handleQuickShare} />
<InfoSection currentText={debouncedText} /> <InfoSection currentText={debouncedText} />
</div> </div>