From 29df6e770f8800a390726cbffde1c62ed7b92f83 Mon Sep 17 00:00:00 2001 From: Clemens Rawert Date: Fri, 19 Jun 2026 16:29:45 +0200 Subject: [PATCH 1/3] Add homepage hero slogan A/B test with PostHog tracking. Rotate the main headline between the existing platform positioning and an agent evals/tracing variant, update the hero subheading, and track exposures in PostHog for conversion analysis. Co-authored-by: Cursor --- components/home/Hero.tsx | 45 +---------- components/home/HeroSlogan.tsx | 143 +++++++++++++++++++++++++++++++++ src/usePostHogClientCapture.ts | 4 + 3 files changed, 149 insertions(+), 43 deletions(-) create mode 100644 components/home/HeroSlogan.tsx diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx index 224ab578bd..9dfffd350d 100644 --- a/components/home/Hero.tsx +++ b/components/home/Hero.tsx @@ -1,12 +1,10 @@ import { Button } from "@/components/ui/button"; import { CornerBox } from "@/components/ui/corner-box"; -import { Heading } from "@/components/ui/heading"; import { Text } from "@/components/ui/text"; -import { TextHighlight } from "@/components/ui"; import { HomeSection } from "@/components/home/HomeSection"; import { EnterpriseLogoGrid } from "@/components/shared/EnterpriseLogoGrid"; -import { cn } from "@/lib/utils"; import { HeroStatsStrip } from "@/components/home/HeroStatsStrip"; +import { HeroSlogan } from "@/components/home/HeroSlogan"; export function Hero() { return ( @@ -15,46 +13,7 @@ export function Hero() { - - - Open Source  - - - - AI - - - Engineering - - - - Platform - - - +
Trace and evaluate AI Agents. Collaborate with your team to diff --git a/components/home/HeroSlogan.tsx b/components/home/HeroSlogan.tsx new file mode 100644 index 0000000000..598f3eddae --- /dev/null +++ b/components/home/HeroSlogan.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePostHog } from "posthog-js/react"; +import { Heading } from "@/components/ui/heading"; +import { TextHighlight } from "@/components/ui"; +import { cn } from "@/lib/utils"; +import { usePostHogClientCapture } from "@/src/usePostHogClientCapture"; + +const STORAGE_KEY = "langfuse-hero-slogan-variant"; + +type HeroSloganVariant = "ai-engineering" | "agent-evals"; + +function pickVariant(): HeroSloganVariant { + return Math.random() < 0.5 ? "ai-engineering" : "agent-evals"; +} + +function readStoredVariant(): HeroSloganVariant | null { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === "ai-engineering" || stored === "agent-evals") { + return stored; + } + } catch { + // localStorage unavailable (e.g. private browsing restrictions) + } + return null; +} + +function persistVariant(variant: HeroSloganVariant) { + try { + localStorage.setItem(STORAGE_KEY, variant); + } catch { + // ignore write failures + } +} + +const headingClassName = cn( + "flex-col items-center gap-0.5 sm:gap-1 md:gap-1.5 text-center font-medium leading-[105%] max-md:max-w-[500px]", + "[leading-trim:both] [text-edge:cap]", +); + +function AiEngineeringSlogan() { + return ( + <> + + Open Source  + + + + AI + + + Engineering + + + + Platform + + + ); +} + +function AgentEvalsSlogan() { + return ( + <> + + Open Source  + + + + Agent + + + Evals + + + and + + + Tracing + + + + ); +} + +export function HeroSlogan() { + const posthog = usePostHog(); + const capture = usePostHogClientCapture(); + const [variant, setVariant] = useState("ai-engineering"); + + useEffect(() => { + const stored = readStoredVariant(); + const nextVariant = stored ?? pickVariant(); + const isNewAssignment = !stored; + + if (!stored) { + persistVariant(nextVariant); + } + + setVariant(nextVariant); + + posthog.register({ hero_slogan_variant: nextVariant }); + capture("hero_slogan_exposure", { + variant: nextVariant, + is_new_assignment: isNewAssignment, + }); + // Track once when the hero mounts on the homepage. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {variant === "ai-engineering" ? ( + + ) : ( + + )} + + ); +} diff --git a/src/usePostHogClientCapture.ts b/src/usePostHogClientCapture.ts index ae19e493f8..f91b8134cf 100644 --- a/src/usePostHogClientCapture.ts +++ b/src/usePostHogClientCapture.ts @@ -5,6 +5,10 @@ import { usePostHog } from "posthog-js/react"; // This preserves existing PostHog event structure while adding type safety interface EventDefinitions { copy_page: { type: "copy" | "chatgpt" | "claude" | "mcp" }; + hero_slogan_exposure: { + variant: "ai-engineering" | "agent-evals"; + is_new_assignment: boolean; + }; } type EventName = keyof EventDefinitions; From 3cfca6ce9adc736ea7f3e59b3d06b2264f428bb9 Mon Sep 17 00:00:00 2001 From: Clemens Rawert Date: Fri, 19 Jun 2026 16:46:30 +0200 Subject: [PATCH 2/3] Fix agent evals hero slogan layout and word spacing. Keep Open Source on its own line and group Agent Evals / and Tracing on the second line with clearer gaps between phrases. Co-authored-by: Cursor --- components/home/HeroSlogan.tsx | 49 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/components/home/HeroSlogan.tsx b/components/home/HeroSlogan.tsx index 598f3eddae..c2f41ecf95 100644 --- a/components/home/HeroSlogan.tsx +++ b/components/home/HeroSlogan.tsx @@ -71,36 +71,39 @@ function AiEngineeringSlogan() { } function AgentEvalsSlogan() { + const wordGap = "gap-x-1.5 sm:gap-x-2"; + const phraseGap = "gap-x-3 sm:gap-x-4"; + return ( <> - Open Source  + Open Source - - - Agent - - - Evals - - - and - - - Tracing - + + + + Agent + + + Evals + + + + + and + + + Tracing + + ); From b7586153711ffb7cc6dead45d0d67ea331f36935 Mon Sep 17 00:00:00 2001 From: Clemens Rawert Date: Fri, 19 Jun 2026 16:59:20 +0200 Subject: [PATCH 3/3] Address hero slogan review: SSR variant, spacing, PostHog timing. Assign the headline variant via middleware cookie and request header so SSR renders the correct slogan without hydration flicker, mirror desktop word spacing on the agent evals line, and defer PostHog registration until the SDK session is ready. Co-authored-by: Cursor --- app/(home)/page.tsx | 19 ++++- components/home/Hero.tsx | 9 ++- components/home/HeroSlogan.tsx | 131 +++++++++++++++++++-------------- components/home/index.tsx | 9 ++- lib/hero-slogan-variant.ts | 22 ++++++ middleware.ts | 38 ++++++++++ 6 files changed, 167 insertions(+), 61 deletions(-) create mode 100644 lib/hero-slogan-variant.ts create mode 100644 middleware.ts diff --git a/app/(home)/page.tsx b/app/(home)/page.tsx index 2ab3c79a07..64c0f1a8ee 100644 --- a/app/(home)/page.tsx +++ b/app/(home)/page.tsx @@ -1,5 +1,20 @@ +import { cookies, headers } from "next/headers"; import { Home } from "@/components/home"; +import { + HERO_SLOGAN_VARIANT_HEADER, + HERO_SLOGAN_VARIANT_KEY, + getHeroSloganVariantFromCookieValue, +} from "@/lib/hero-slogan-variant"; -export default function HomePage() { - return ; +export default async function HomePage() { + const [cookieStore, headerStore] = await Promise.all([ + cookies(), + headers(), + ]); + const heroSloganVariant = getHeroSloganVariantFromCookieValue( + headerStore.get(HERO_SLOGAN_VARIANT_HEADER) ?? + cookieStore.get(HERO_SLOGAN_VARIANT_KEY)?.value, + ); + + return ; } diff --git a/components/home/Hero.tsx b/components/home/Hero.tsx index 9dfffd350d..b3c82a5257 100644 --- a/components/home/Hero.tsx +++ b/components/home/Hero.tsx @@ -5,15 +5,20 @@ import { HomeSection } from "@/components/home/HomeSection"; import { EnterpriseLogoGrid } from "@/components/shared/EnterpriseLogoGrid"; import { HeroStatsStrip } from "@/components/home/HeroStatsStrip"; import { HeroSlogan } from "@/components/home/HeroSlogan"; +import type { HeroSloganVariant } from "@/lib/hero-slogan-variant"; -export function Hero() { +export function Hero({ + heroSloganVariant, +}: { + heroSloganVariant: HeroSloganVariant; +}) { return ( - +
Trace and evaluate AI Agents. Collaborate with your team to diff --git a/components/home/HeroSlogan.tsx b/components/home/HeroSlogan.tsx index c2f41ecf95..f0e7459df6 100644 --- a/components/home/HeroSlogan.tsx +++ b/components/home/HeroSlogan.tsx @@ -1,23 +1,19 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; import { usePostHog } from "posthog-js/react"; import { Heading } from "@/components/ui/heading"; import { TextHighlight } from "@/components/ui"; import { cn } from "@/lib/utils"; import { usePostHogClientCapture } from "@/src/usePostHogClientCapture"; - -const STORAGE_KEY = "langfuse-hero-slogan-variant"; - -type HeroSloganVariant = "ai-engineering" | "agent-evals"; - -function pickVariant(): HeroSloganVariant { - return Math.random() < 0.5 ? "ai-engineering" : "agent-evals"; -} +import { + HERO_SLOGAN_VARIANT_KEY, + type HeroSloganVariant, +} from "@/lib/hero-slogan-variant"; function readStoredVariant(): HeroSloganVariant | null { try { - const stored = localStorage.getItem(STORAGE_KEY); + const stored = localStorage.getItem(HERO_SLOGAN_VARIANT_KEY); if (stored === "ai-engineering" || stored === "agent-evals") { return stored; } @@ -29,7 +25,7 @@ function readStoredVariant(): HeroSloganVariant | null { function persistVariant(variant: HeroSloganVariant) { try { - localStorage.setItem(STORAGE_KEY, variant); + localStorage.setItem(HERO_SLOGAN_VARIANT_KEY, variant); } catch { // ignore write failures } @@ -40,6 +36,8 @@ const headingClassName = cn( "[leading-trim:both] [text-edge:cap]", ); +const desktopWordSpacing = "max-[499px]:pr-1.75 min-[500px]:pr-2"; + function AiEngineeringSlogan() { return ( <> @@ -49,10 +47,10 @@ function AiEngineeringSlogan() { > Open Source  - + AI @@ -71,9 +69,6 @@ function AiEngineeringSlogan() { } function AgentEvalsSlogan() { - const wordGap = "gap-x-1.5 sm:gap-x-2"; - const phraseGap = "gap-x-3 sm:gap-x-4"; - return ( <> Open Source - - - - Agent - - - Evals - - - - - and - - - Tracing - - + + + Agent + + + Evals + + + and + + + Tracing + ); } -export function HeroSlogan() { +function trackHeroSloganExposure( + posthog: ReturnType, + capture: ReturnType, + variant: HeroSloganVariant, + isNewAssignment: boolean, +) { + posthog.register({ hero_slogan_variant: variant }); + capture("hero_slogan_exposure", { + variant, + is_new_assignment: isNewAssignment, + }); +} + +export function HeroSlogan({ + initialVariant, +}: { + initialVariant: HeroSloganVariant; +}) { const posthog = usePostHog(); const capture = usePostHogClientCapture(); - const [variant, setVariant] = useState("ai-engineering"); + const trackedRef = useRef(false); useEffect(() => { - const stored = readStoredVariant(); - const nextVariant = stored ?? pickVariant(); - const isNewAssignment = !stored; + const hadLocalStorage = readStoredVariant() !== null; + persistVariant(initialVariant); - if (!stored) { - persistVariant(nextVariant); + if (trackedRef.current) { + return; } - setVariant(nextVariant); + const runTracking = () => { + if (trackedRef.current) { + return; + } + trackedRef.current = true; + trackHeroSloganExposure( + posthog, + capture, + initialVariant, + !hadLocalStorage, + ); + }; + + if ((posthog as { __loaded?: boolean }).__loaded) { + runTracking(); + return; + } - posthog.register({ hero_slogan_variant: nextVariant }); - capture("hero_slogan_exposure", { - variant: nextVariant, - is_new_assignment: isNewAssignment, - }); - // Track once when the hero mounts on the homepage. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + posthog.onSessionId(runTracking); + }, [capture, initialVariant, posthog]); return ( - {variant === "ai-engineering" ? ( + {initialVariant === "ai-engineering" ? ( ) : ( diff --git a/components/home/index.tsx b/components/home/index.tsx index 19572ac1cd..2c0750d410 100644 --- a/components/home/index.tsx +++ b/components/home/index.tsx @@ -9,11 +9,16 @@ import { Enterprise } from "./Enterprise"; import { WhyLangfuse } from "./WhyLangfuse"; import { GetStartedSection } from "./GetStartedSection"; import { FAQ } from "./FAQ"; +import type { HeroSloganVariant } from "@/lib/hero-slogan-variant"; -export const Home = () => ( +export const Home = ({ + heroSloganVariant, +}: { + heroSloganVariant: HeroSloganVariant; +}) => ( <>
- + diff --git a/lib/hero-slogan-variant.ts b/lib/hero-slogan-variant.ts new file mode 100644 index 0000000000..36859c1c4d --- /dev/null +++ b/lib/hero-slogan-variant.ts @@ -0,0 +1,22 @@ +export const HERO_SLOGAN_VARIANT_HEADER = "x-hero-slogan-variant"; + +export const HERO_SLOGAN_VARIANT_KEY = "langfuse-hero-slogan-variant"; + +export type HeroSloganVariant = "ai-engineering" | "agent-evals"; + +export function isHeroSloganVariant(value: string): value is HeroSloganVariant { + return value === "ai-engineering" || value === "agent-evals"; +} + +export function pickHeroSloganVariant(): HeroSloganVariant { + return Math.random() < 0.5 ? "ai-engineering" : "agent-evals"; +} + +export function getHeroSloganVariantFromCookieValue( + value: string | undefined, +): HeroSloganVariant { + if (value && isHeroSloganVariant(value)) { + return value; + } + return pickHeroSloganVariant(); +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000000..7e82141b6c --- /dev/null +++ b/middleware.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { + HERO_SLOGAN_VARIANT_KEY, + isHeroSloganVariant, + pickHeroSloganVariant, +} from "@/lib/hero-slogan-variant"; + +const HERO_SLOGAN_VARIANT_HEADER = "x-hero-slogan-variant"; + +export function middleware(request: NextRequest) { + const existing = request.cookies.get(HERO_SLOGAN_VARIANT_KEY)?.value; + const variant = + existing && isHeroSloganVariant(existing) + ? existing + : pickHeroSloganVariant(); + + const requestHeaders = new Headers(request.headers); + requestHeaders.set(HERO_SLOGAN_VARIANT_HEADER, variant); + + const response = NextResponse.next({ + request: { headers: requestHeaders }, + }); + + if (!existing || !isHeroSloganVariant(existing)) { + response.cookies.set(HERO_SLOGAN_VARIANT_KEY, variant, { + maxAge: 60 * 60 * 24 * 365, + path: "/", + sameSite: "lax", + }); + } + + return response; +} + +export const config = { + matcher: "/", +};