Compare commits

..

5 Commits

Author SHA1 Message Date
Tim Leikauf
4274d680e1 Rework PostHog setup — use Next.js ingest proxy + posthog-js/react
Mirrors the proven pattern from gendersloty: rewrites /ingest/* to
the PostHog host so requests go through the same origin (no CORS,
no ad-blocker issues). Uses PostHogProvider and usePostHog from
posthog-js/react official React integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:50:43 +02:00
Tim Leikauf
eb228ba50b Test PostHog with EU cloud host to isolate proxy issue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:48:16 +02:00
Tim Leikauf
8601119f64 Switch PostHog persistence to memory
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:16:15 +02:00
Tim Leikauf
f487189a19 Fix PostHog init race condition — remove __loaded guard
posthog.capture() queues events automatically before init completes,
so checking __loaded blocked the first pageview from being sent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:04:57 +02:00
Tim Leikauf
0101447d84 Add PostHog analytics — full event tracking across all pages
Tracks pageviews, hero/CTA clicks, product teaser clicks, app card
and detail views, newsletter signups, nav interactions, and footer
link clicks with structured properties for PostHog dashboards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 20:59:29 +02:00
16 changed files with 282 additions and 66 deletions

View File

@@ -1,10 +1,8 @@
import type { Metadata } from 'next' 'use client'
import Link from 'next/link'
export const metadata: Metadata = { import Link from 'next/link'
title: 'Fittimo Bewegung im Alltag | HyggeCraftery', import { useEffect } from 'react'
description: 'Sanfte Bewegung und Atemübungen für Körper und Geist im Einklang.', import { posthog } from '@/lib/posthog'
}
const accent = '#7DAF8A' const accent = '#7DAF8A'
@@ -16,6 +14,10 @@ const features = [
] ]
export default function FittimoPage() { export default function FittimoPage() {
useEffect(() => {
posthog.capture('app_detail_viewed', { app: 'fittimo' })
}, [])
return ( return (
<div style={{ fontFamily: 'var(--font-mulish, Mulish, sans-serif)', color: '#3D2B1F', background: '#FAFAF7', overflow: 'hidden' }}> <div style={{ fontFamily: 'var(--font-mulish, Mulish, sans-serif)', color: '#3D2B1F', background: '#FAFAF7', overflow: 'hidden' }}>
@@ -98,7 +100,11 @@ export default function FittimoPage() {
<p style={{ fontSize: 16, color: 'rgba(250,250,247,0.65)', margin: '0 0 40px', lineHeight: 1.7 }}> <p style={{ fontSize: 16, color: 'rgba(250,250,247,0.65)', margin: '0 0 40px', lineHeight: 1.7 }}>
Fittimo erscheint bald. Trage dich in unseren Newsletter ein und erfahre als Erste davon. Fittimo erscheint bald. Trage dich in unseren Newsletter ein und erfahre als Erste davon.
</p> </p>
<Link href="/#newsletter" style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, background: accent, color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, padding: '16px 36px', borderRadius: 44 }}> <Link
href="/#newsletter"
onClick={() => posthog.capture('newsletter_cta_clicked', { source: 'app_page', app_name: 'fittimo' })}
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, background: accent, color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, padding: '16px 36px', borderRadius: 44 }}
>
Benachrichtigen lassen Benachrichtigen lassen
</Link> </Link>
</div> </div>

View File

@@ -1,10 +1,7 @@
import type { Metadata } from 'next' 'use client'
import Link from 'next/link'
export const metadata: Metadata = { import Link from 'next/link'
title: 'Apps HyggeCraftery', import { posthog } from '@/lib/posthog'
description: 'Drei sanfte Apps für einen langsameren, bewussteren Alltag.',
}
const apps = [ const apps = [
{ {
@@ -56,6 +53,7 @@ export default function AppsPage() {
<Link <Link
key={app.name} key={app.name}
href={app.href} href={app.href}
onClick={() => posthog.capture('app_card_clicked', { app_name: app.name })}
style={{ style={{
textDecoration: 'none', textDecoration: 'none',
display: 'grid', display: 'grid',

View File

@@ -1,10 +1,8 @@
import type { Metadata } from 'next' 'use client'
import Link from 'next/link'
export const metadata: Metadata = { import Link from 'next/link'
title: 'Rezeptimo Kochen mit Freude | HyggeCraftery', import { useEffect } from 'react'
description: 'Saisonale Rezepte zum langsamen Kochen — ehrlich, einfach, nährend.', import { posthog } from '@/lib/posthog'
}
const accent = '#C4896A' const accent = '#C4896A'
@@ -16,6 +14,10 @@ const features = [
] ]
export default function RezeptimoPage() { export default function RezeptimoPage() {
useEffect(() => {
posthog.capture('app_detail_viewed', { app: 'rezeptimo' })
}, [])
return ( return (
<div style={{ fontFamily: 'var(--font-mulish, Mulish, sans-serif)', color: '#3D2B1F', background: '#FAFAF7', overflow: 'hidden' }}> <div style={{ fontFamily: 'var(--font-mulish, Mulish, sans-serif)', color: '#3D2B1F', background: '#FAFAF7', overflow: 'hidden' }}>
@@ -98,7 +100,11 @@ export default function RezeptimoPage() {
<p style={{ fontSize: 16, color: 'rgba(250,250,247,0.65)', margin: '0 0 40px', lineHeight: 1.7 }}> <p style={{ fontSize: 16, color: 'rgba(250,250,247,0.65)', margin: '0 0 40px', lineHeight: 1.7 }}>
Rezeptimo erscheint bald. Trage dich in unseren Newsletter ein und erfahre als Erste davon. Rezeptimo erscheint bald. Trage dich in unseren Newsletter ein und erfahre als Erste davon.
</p> </p>
<Link href="/#newsletter" style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, background: accent, color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, padding: '16px 36px', borderRadius: 44 }}> <Link
href="/#newsletter"
onClick={() => posthog.capture('newsletter_cta_clicked', { source: 'app_page', app_name: 'rezeptimo' })}
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, background: accent, color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, padding: '16px 36px', borderRadius: 44 }}
>
Benachrichtigen lassen Benachrichtigen lassen
</Link> </Link>
</div> </div>

View File

@@ -1,10 +1,8 @@
import type { Metadata } from 'next' 'use client'
import Link from 'next/link'
export const metadata: Metadata = { import Link from 'next/link'
title: 'Snakkimo Sprachen lernen | HyggeCraftery', import { useEffect } from 'react'
description: 'Sprachen lernen, das sich anfühlt wie durch einen schönen Feed scrollen. Kleine Lektionen, großer Effekt.', import { posthog } from '@/lib/posthog'
}
const accent = '#7BA7BC' const accent = '#7BA7BC'
@@ -16,6 +14,10 @@ const features = [
] ]
export default function SnakkimoPage() { export default function SnakkimoPage() {
useEffect(() => {
posthog.capture('app_detail_viewed', { app: 'snakkimo' })
}, [])
return ( return (
<div style={{ fontFamily: 'var(--font-mulish, Mulish, sans-serif)', color: '#3D2B1F', background: '#FAFAF7', overflow: 'hidden' }}> <div style={{ fontFamily: 'var(--font-mulish, Mulish, sans-serif)', color: '#3D2B1F', background: '#FAFAF7', overflow: 'hidden' }}>
@@ -98,7 +100,11 @@ export default function SnakkimoPage() {
<p style={{ fontSize: 16, color: 'rgba(250,250,247,0.65)', margin: '0 0 40px', lineHeight: 1.7 }}> <p style={{ fontSize: 16, color: 'rgba(250,250,247,0.65)', margin: '0 0 40px', lineHeight: 1.7 }}>
Snakkimo erscheint bald. Trage dich in unseren Newsletter ein und erfahre als Erste davon. Snakkimo erscheint bald. Trage dich in unseren Newsletter ein und erfahre als Erste davon.
</p> </p>
<Link href="/#newsletter" style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, background: accent, color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, padding: '16px 36px', borderRadius: 44 }}> <Link
href="/#newsletter"
onClick={() => posthog.capture('newsletter_cta_clicked', { source: 'app_page', app_name: 'snakkimo' })}
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 10, background: accent, color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, padding: '16px 36px', borderRadius: 44 }}
>
Benachrichtigen lassen Benachrichtigen lassen
</Link> </Link>
</div> </div>

8
app/layout.tsx Normal file → Executable file
View File

@@ -3,6 +3,9 @@ import { Cormorant_Garamond, Mulish } from 'next/font/google'
import './globals.css' import './globals.css'
import Nav from '@/components/ui/Nav' import Nav from '@/components/ui/Nav'
import Footer from '@/components/ui/Footer' import Footer from '@/components/ui/Footer'
import PostHogProvider from '@/components/PostHogProvider'
import { PostHogPageView } from '@/components/PostHogPageView'
import { Suspense } from 'react'
const cormorant = Cormorant_Garamond({ const cormorant = Cormorant_Garamond({
subsets: ['latin'], subsets: ['latin'],
@@ -45,9 +48,14 @@ export default function RootLayout({
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`, backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
}} }}
/> />
<PostHogProvider>
<Suspense fallback={null}>
<PostHogPageView />
</Suspense>
<Nav /> <Nav />
<main>{children}</main> <main>{children}</main>
<Footer /> <Footer />
</PostHogProvider>
</body> </body>
</html> </html>
) )

View File

@@ -1,6 +1,9 @@
'use client'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import NewsletterForm from '@/components/ui/NewsletterForm' import NewsletterForm from '@/components/ui/NewsletterForm'
import { posthog } from '@/lib/posthog'
export default function HomePage() { export default function HomePage() {
return ( return (
@@ -21,11 +24,19 @@ export default function HomePage() {
Handgemachte Stücke und sanfte Rituale für ein Zuhause, das zur Ruhe einlädt. Wir feiern das Langsame, das Echte, das Wesentliche. Handgemachte Stücke und sanfte Rituale für ein Zuhause, das zur Ruhe einlädt. Wir feiern das Langsame, das Echte, das Wesentliche.
</p> </p>
<div style={{ display: 'flex', alignItems: 'center', gap: 22 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 22 }}>
<Link href="/shop" style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 11, background: '#C4896A', color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, letterSpacing: 0.3, padding: '16px 32px', borderRadius: 44 }}> <Link
href="/shop"
onClick={() => posthog.capture('hero_cta_clicked', { destination: '/shop', label: 'Entdecken' })}
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 11, background: '#C4896A', color: '#FAFAF7', fontSize: 15.5, fontWeight: 600, letterSpacing: 0.3, padding: '16px 32px', borderRadius: 44 }}
>
Entdecken Entdecken
<span style={{ display: 'inline-block', width: 18, height: 18, borderRadius: '50%', border: '1.5px solid rgba(250,250,247,0.6)' }} /> <span style={{ display: 'inline-block', width: 18, height: 18, borderRadius: '50%', border: '1.5px solid rgba(250,250,247,0.6)' }} />
</Link> </Link>
<Link href="/#ueber" style={{ textDecoration: 'none', fontSize: 15, fontWeight: 600, color: '#3D2B1F', borderBottom: '1.5px solid #A8B89A', paddingBottom: 3 }}> <Link
href="/#ueber"
onClick={() => posthog.capture('hero_cta_clicked', { destination: '/#ueber', label: 'Unsere Geschichte' })}
style={{ textDecoration: 'none', fontSize: 15, fontWeight: 600, color: '#3D2B1F', borderBottom: '1.5px solid #A8B89A', paddingBottom: 3 }}
>
Unsere Geschichte Unsere Geschichte
</Link> </Link>
</div> </div>
@@ -68,14 +79,23 @@ export default function HomePage() {
Stücke fürs Zuhause Stücke fürs Zuhause
</h2> </h2>
</div> </div>
<Link href="/shop" style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 11, border: '1.5px solid #3D2B1F', color: '#3D2B1F', fontSize: 15, fontWeight: 600, padding: '14px 28px', borderRadius: 44 }}> <Link
href="/shop"
onClick={() => posthog.capture('hero_cta_clicked', { destination: '/shop', label: 'Zum Shop' })}
style={{ textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 11, border: '1.5px solid #3D2B1F', color: '#3D2B1F', fontSize: 15, fontWeight: 600, padding: '14px 28px', borderRadius: 44 }}
>
Zum Shop <span style={{ fontSize: 18, lineHeight: 1 }}></span> Zum Shop <span style={{ fontSize: 18, lineHeight: 1 }}></span>
</Link> </Link>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 30 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 30 }}>
{shopProducts.map((p) => ( {shopProducts.map((p) => (
<Link key={p.name} href="/shop" style={{ textDecoration: 'none', color: 'inherit', display: 'block' }}> <Link
key={p.name}
href="/shop"
onClick={() => posthog.capture('product_teaser_clicked', { product_name: p.name, price: p.price })}
style={{ textDecoration: 'none', color: 'inherit', display: 'block' }}
>
<div style={{ position: 'relative', height: 360, borderRadius: 30, overflow: 'hidden', background: p.bg, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', padding: 18 }}> <div style={{ position: 'relative', height: 360, borderRadius: 30, overflow: 'hidden', background: p.bg, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', padding: 18 }}>
{p.badge && ( {p.badge && (
<span style={{ position: 'absolute', top: 18, left: 18, background: '#A8B89A', color: '#FAFAF7', fontSize: 11.5, fontWeight: 700, letterSpacing: 0.6, padding: '6px 13px', borderRadius: 30 }}> <span style={{ position: 'absolute', top: 18, left: 18, background: '#A8B89A', color: '#FAFAF7', fontSize: 11.5, fontWeight: 700, letterSpacing: 0.6, padding: '6px 13px', borderRadius: 30 }}>
@@ -117,7 +137,11 @@ export default function HomePage() {
</div> </div>
<h3 style={{ fontFamily: 'var(--font-cormorant, "Cormorant Garamond", serif)', fontWeight: 600, fontSize: 27, margin: '0 0 10px', color: '#FAFAF7' }}>{app.name}</h3> <h3 style={{ fontFamily: 'var(--font-cormorant, "Cormorant Garamond", serif)', fontWeight: 600, fontSize: 27, margin: '0 0 10px', color: '#FAFAF7' }}>{app.name}</h3>
<p style={{ fontSize: 14.5, color: 'rgba(250,250,247,0.6)', margin: '0 0 22px' }}>{app.desc}</p> <p style={{ fontSize: 14.5, color: 'rgba(250,250,247,0.6)', margin: '0 0 22px' }}>{app.desc}</p>
<Link href={app.href} style={{ textDecoration: 'none', fontSize: 14.5, fontWeight: 600, color: app.linkColor, borderBottom: `1.5px solid ${app.linkColor}50`, paddingBottom: 2 }}> <Link
href={app.href}
onClick={() => posthog.capture('app_teaser_clicked', { app_name: app.name })}
style={{ textDecoration: 'none', fontSize: 14.5, fontWeight: 600, color: app.linkColor, borderBottom: `1.5px solid ${app.linkColor}50`, paddingBottom: 2 }}
>
Mehr erfahren Mehr erfahren
</Link> </Link>
</div> </div>
@@ -139,7 +163,7 @@ export default function HomePage() {
<p style={{ fontSize: 16, color: '#8a7a68', margin: '0 auto 36px', maxWidth: 460 }}> <p style={{ fontSize: 16, color: '#8a7a68', margin: '0 auto 36px', maxWidth: 460 }}>
Sanfte Geschichten, saisonale Rituale und stille Inspiration etwa einmal im Monat, ganz ohne Eile. Sanfte Geschichten, saisonale Rituale und stille Inspiration etwa einmal im Monat, ganz ohne Eile.
</p> </p>
<NewsletterForm /> <NewsletterForm source="homepage" />
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,10 +1,7 @@
import type { Metadata } from 'next' 'use client'
import Link from 'next/link'
export const metadata: Metadata = { import Link from 'next/link'
title: 'Shop HyggeCraftery', import { posthog } from '@/lib/posthog'
description: 'Handgemachte Stücke für ein langsameres, schöneres Zuhause.',
}
const products = [ const products = [
{ name: 'Leinen-Kissen «Fjord»', price: '€ 48', desc: 'Sanftes Leinen in gedämpftem Salbei.', badge: 'Neu', bg: 'repeating-linear-gradient(40deg,#F0E8DA,#F0E8DA 11px,#E7DDCC 11px,#E7DDCC 22px)' }, { name: 'Leinen-Kissen «Fjord»', price: '€ 48', desc: 'Sanftes Leinen in gedämpftem Salbei.', badge: 'Neu', bg: 'repeating-linear-gradient(40deg,#F0E8DA,#F0E8DA 11px,#E7DDCC 11px,#E7DDCC 22px)' },
@@ -46,7 +43,11 @@ export default function ShopPage() {
{/* Product grid */} {/* Product grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 30 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 30 }}>
{products.map((p) => ( {products.map((p) => (
<div key={p.name} style={{ display: 'block', color: 'inherit' }}> <div
key={p.name}
onClick={() => posthog.capture('product_viewed', { product_name: p.name, price: p.price })}
style={{ display: 'block', color: 'inherit', cursor: 'pointer' }}
>
<div style={{ position: 'relative', height: 360, borderRadius: 30, overflow: 'hidden', background: p.bg, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', padding: 18 }}> <div style={{ position: 'relative', height: 360, borderRadius: 30, overflow: 'hidden', background: p.bg, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', padding: 18 }}>
{p.badge && ( {p.badge && (
<span style={{ position: 'absolute', top: 18, left: 18, background: p.badge === 'Bald' ? '#C4896A' : '#A8B89A', color: '#FAFAF7', fontSize: 11.5, fontWeight: 700, letterSpacing: 0.6, padding: '6px 13px', borderRadius: 30 }}> <span style={{ position: 'absolute', top: 18, left: 18, background: p.badge === 'Bald' ? '#C4896A' : '#A8B89A', color: '#FAFAF7', fontSize: 11.5, fontWeight: 700, letterSpacing: 0.6, padding: '6px 13px', borderRadius: 30 }}>

View File

@@ -0,0 +1,22 @@
'use client'
import { usePathname, useSearchParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import { useEffect } from 'react'
export function PostHogPageView() {
const pathname = usePathname()
const searchParams = useSearchParams()
const posthog = usePostHog()
useEffect(() => {
if (pathname && posthog) {
let url = window.origin + pathname
const search = searchParams.toString()
if (search) url += `?${search}`
posthog.capture('$pageview', { $current_url: url })
}
}, [pathname, searchParams, posthog])
return null
}

View File

@@ -0,0 +1,18 @@
'use client'
import posthog from 'posthog-js'
import { PostHogProvider as PHProvider } from 'posthog-js/react'
import { useEffect } from 'react'
export default function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
posthog.init('phc_BHgg9S7CQqVShe7EMCdi86PxA49qcNaTsR9Nn5EGxRCT', {
api_host: '/ingest',
ui_host: 'https://analytics.hyggecraftery.com',
capture_pageview: false,
persistence: 'memory',
})
}, [])
return <PHProvider client={posthog}>{children}</PHProvider>
}

View File

@@ -1,4 +1,7 @@
'use client'
import Link from 'next/link' import Link from 'next/link'
import { posthog } from '@/lib/posthog'
export default function Footer() { export default function Footer() {
return ( return (
@@ -25,10 +28,10 @@ export default function Footer() {
Entdecken Entdecken
</h4> </h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<FooterLink href="/shop">Shop</FooterLink> <FooterLink href="/shop" section="Entdecken">Shop</FooterLink>
<FooterLink href="/apps">Apps</FooterLink> <FooterLink href="/apps" section="Entdecken">Apps</FooterLink>
<FooterLink href="/#ueber">Über uns</FooterLink> <FooterLink href="/#ueber" section="Entdecken">Über uns</FooterLink>
<FooterLink href="/#newsletter">Newsletter</FooterLink> <FooterLink href="/#newsletter" section="Entdecken">Newsletter</FooterLink>
</div> </div>
</div> </div>
@@ -38,10 +41,10 @@ export default function Footer() {
Service Service
</h4> </h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<FooterLink href="#">Versand</FooterLink> <FooterLink href="#" section="Service">Versand</FooterLink>
<FooterLink href="#">Kontakt</FooterLink> <FooterLink href="#" section="Service">Kontakt</FooterLink>
<FooterLink href="#">Impressum</FooterLink> <FooterLink href="#" section="Service">Impressum</FooterLink>
<FooterLink href="#">Datenschutz</FooterLink> <FooterLink href="#" section="Service">Datenschutz</FooterLink>
</div> </div>
</div> </div>
@@ -56,6 +59,7 @@ export default function Footer() {
key={abbr} key={abbr}
href="#" href="#"
title={label} title={label}
onClick={() => posthog.capture('footer_link_clicked', { label, section: 'Folgen' })}
style={{ style={{
textDecoration: 'none', textDecoration: 'none',
width: 42, width: 42,
@@ -86,9 +90,13 @@ export default function Footer() {
) )
} }
function FooterLink({ href, children }: { href: string; children: React.ReactNode }) { function FooterLink({ href, section, children }: { href: string; section: string; children: React.ReactNode }) {
return ( return (
<Link href={href} style={{ textDecoration: 'none', fontSize: 14.5, color: 'rgba(250,250,247,0.7)' }}> <Link
href={href}
onClick={() => posthog.capture('footer_link_clicked', { label: String(children), section })}
style={{ textDecoration: 'none', fontSize: 14.5, color: 'rgba(250,250,247,0.7)' }}
>
{children} {children}
</Link> </Link>
) )

View File

@@ -2,6 +2,7 @@
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { useState } from 'react'
import { posthog } from '@/lib/posthog'
const apps = [ const apps = [
{ name: 'Snakkimo', href: '/apps/snakkimo', accent: '#7BA7BC' }, { name: 'Snakkimo', href: '/apps/snakkimo', accent: '#7BA7BC' },
@@ -26,7 +27,7 @@ export default function Nav() {
}} }}
> >
{/* Logo */} {/* Logo */}
<Link href="/" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 12 }}> <Link href="/" onClick={() => posthog.capture('nav_link_clicked', { label: 'Logo', href: '/' })} style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 12 }}>
<div <div
style={{ style={{
width: 34, width: 34,
@@ -50,8 +51,8 @@ export default function Nav() {
{/* Nav links */} {/* Nav links */}
<nav style={{ display: 'flex', alignItems: 'center', gap: 38 }}> <nav style={{ display: 'flex', alignItems: 'center', gap: 38 }}>
<Link href="/#ueber" style={navLinkStyle}>Über uns</Link> <Link href="/#ueber" onClick={() => posthog.capture('nav_link_clicked', { label: 'Über uns', href: '/#ueber' })} style={navLinkStyle}>Über uns</Link>
<Link href="/shop" style={navLinkStyle}>Shop</Link> <Link href="/shop" onClick={() => posthog.capture('nav_link_clicked', { label: 'Shop', href: '/shop' })} style={navLinkStyle}>Shop</Link>
{/* Apps dropdown */} {/* Apps dropdown */}
<div <div
@@ -61,7 +62,11 @@ export default function Nav() {
> >
<button <button
style={{ ...navLinkStyle, background: 'none', border: 'none', cursor: 'pointer', padding: 0 }} style={{ ...navLinkStyle, background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}
onClick={() => setAppsOpen((v) => !v)} onClick={() => {
const next = !appsOpen
setAppsOpen(next)
if (next) posthog.capture('nav_apps_dropdown_opened')
}}
aria-expanded={appsOpen} aria-expanded={appsOpen}
> >
Apps Apps
@@ -98,7 +103,7 @@ export default function Nav() {
fontSize: 14.5, fontSize: 14.5,
fontWeight: 500, fontWeight: 500,
}} }}
onClick={() => setAppsOpen(false)} onClick={() => { setAppsOpen(false); posthog.capture('nav_link_clicked', { label: app.name, href: app.href }) }}
> >
<span <span
style={{ style={{
@@ -118,6 +123,7 @@ export default function Nav() {
<Link <Link
href="/#newsletter" href="/#newsletter"
onClick={() => posthog.capture('newsletter_cta_clicked', { source: 'nav' })}
style={{ style={{
textDecoration: 'none', textDecoration: 'none',
color: '#FAFAF7', color: '#FAFAF7',

8
components/ui/NewsletterForm.tsx Normal file → Executable file
View File

@@ -1,8 +1,9 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { posthog } from '@/lib/posthog'
export default function NewsletterForm() { export default function NewsletterForm({ source = 'homepage' }: { source?: string }) {
const [email, setEmail] = useState('') const [email, setEmail] = useState('')
const [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
@@ -21,7 +22,10 @@ export default function NewsletterForm() {
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
if (email.trim()) setSubmitted(true) if (email.trim()) {
posthog.capture('newsletter_signup', { source })
setSubmitted(true)
}
}} }}
style={{ display: 'flex', gap: 12, maxWidth: 480, margin: '0 auto', flexWrap: 'wrap' }} style={{ display: 'flex', gap: 12, maxWidth: 480, margin: '0 auto', flexWrap: 'wrap' }}
> >

3
lib/posthog.ts Normal file
View File

@@ -0,0 +1,3 @@
import posthog from 'posthog-js'
export { posthog }
export { usePostHog } from 'posthog-js/react'

View File

@@ -1,4 +1,22 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = {}; const nextConfig = {
async rewrites() {
return [
{
source: '/ingest/static/:path*',
destination: 'https://analytics.hyggecraftery.com/static/:path*',
},
{
source: '/ingest/:path*',
destination: 'https://analytics.hyggecraftery.com/:path*',
},
{
source: '/ingest/decide',
destination: 'https://analytics.hyggecraftery.com/decide',
},
]
},
skipTrailingSlashRedirect: true,
}
export default nextConfig; export default nextConfig

87
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"next": "14.2.35", "next": "14.2.35",
"posthog-js": "^1.396.4",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18"
}, },
@@ -273,6 +274,21 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@posthog/core": {
"version": "1.39.3",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.39.3.tgz",
"integrity": "sha512-oR+B8Q5O61N+W2+HVOBG9dbxAT/+OVxX+XvpNj6KYgptN2EB14JiKQ9Rm7DNpErNTLV7WApZEK/URkZgldOxfg==",
"license": "MIT",
"dependencies": {
"@posthog/types": "^1.392.0"
}
},
"node_modules/@posthog/types": {
"version": "1.392.0",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.392.0.tgz",
"integrity": "sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==",
"license": "MIT"
},
"node_modules/@swc/counter": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -327,6 +343,13 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/any-promise": { "node_modules/any-promise": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -476,6 +499,17 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -510,6 +544,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dompurify": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
"integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/es-errors": { "node_modules/es-errors": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
@@ -560,6 +603,12 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1111,6 +1160,38 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/posthog-js": {
"version": "1.396.4",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.396.4.tgz",
"integrity": "sha512-PycBmwKQD1T7YFYrGRb8rjQET/UVnexgUy8gVe6UBEhwHXEIhZF4na5VakJbn4zu1wg4tzjt8r7PA4VLu6bDjg==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@posthog/core": "^1.39.3",
"@posthog/types": "^1.392.0",
"core-js": "^3.38.1",
"dompurify": "^3.3.2",
"fflate": "^0.4.8",
"preact": "^10.29.2",
"query-selector-shadow-dom": "^1.0.1",
"web-vitals": "^5.3.0"
}
},
"node_modules/preact": {
"version": "10.29.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz",
"integrity": "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/query-selector-shadow-dom": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
"integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
"license": "MIT"
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -1484,6 +1565,12 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
},
"node_modules/web-vitals": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.3.0.tgz",
"integrity": "sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==",
"license": "Apache-2.0"
} }
} }
} }

9
package.json Normal file → Executable file
View File

@@ -9,16 +9,17 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"next": "14.2.35",
"posthog-js": "^1.396.4",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18"
"next": "14.2.35"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1",
"typescript": "^5"
} }
} }