Posthog integration
This commit is contained in:
parent
878dbc63f7
commit
1179b406b8
|
|
@ -0,0 +1,2 @@
|
||||||
|
NEXT_PUBLIC_POSTHOG_KEY=phc_xxx
|
||||||
|
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
227
pages/index.jsx
227
pages/index.jsx
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
@ -252,8 +426,7 @@ export default function HomePage() {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|
||||||
<SocialButtons onShare={handleQuickShare} />
|
<SocialButtons onShare={handleQuickShare} />
|
||||||
<InfoSection currentText={debouncedText} />
|
<InfoSection currentText={debouncedText} />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue