@@ -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
-
+
-
+
-
+
@@ -189,7 +355,15 @@ export default function HomePage() {
{
+ setAnimationsEnabled(val);
+ posthog.capture("animations_toggled", { enabled: val });
+ serverTrack({
+ distinctId: "anon_client",
+ event: "animations_toggled",
+ properties: { enabled: val },
+ });
+ }}
totalFonts={Object.keys(fontTransforms).length}
onQuickShare={handleQuickShare}
/>
@@ -232,28 +406,27 @@ export default function HomePage() {
)}
-
-
- {filteredFonts.map((name, i) => (
-
- ))}
-
-
-
+
+
+ {filteredFonts.map((name, i) => (
+
+ ))}
+
+