From fcdce11d657cc172e4773889e720d2a34c6f1d46 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Wed, 29 Apr 2026 06:53:17 -0400 Subject: [PATCH 1/4] Add Statsig-driven marketing hero layouts with query overrides Introduce hero_layout experiment (0 aside, 1 bottom two-line title, 2 bottom one-line title) evaluated on the server and mirrored on the client for exposure logging. Add URL query overrides for title, subtitle, and layout, optional variation config heroLayout, and dashboard placement styling. Tighten bottom-layout spacing, align CTA-to-screenshot gap with nav-to-banner padding, and add dev-only Statsig experiment console logging (plus ?debug_statsig for preview builds). Made-with: Cursor --- .../components/AppwriteIn100Seconds.svelte | 2 +- .../homepage-variations/custom-hero.svelte | 95 +++++++-- .../homepage-variations/variation-config.ts | 4 + src/lib/statsig/client.ts | 7 +- src/lib/statsig/constants.ts | 5 + src/lib/statsig/hero-layout.ts | 13 ++ src/lib/statsig/hero-query-overrides.ts | 67 ++++++ src/lib/statsig/hero-statsig.server.ts | 32 ++- .../(marketing)/(components)/dashboard.svelte | 10 +- .../(marketing)/(components)/hero.svelte | 200 ++++++++++++++---- src/routes/(marketing)/+page.server.ts | 19 +- src/routes/(marketing)/+page.svelte | 2 +- src/routes/[variation]/+page.svelte | 1 + 13 files changed, 387 insertions(+), 70 deletions(-) create mode 100644 src/lib/statsig/hero-layout.ts create mode 100644 src/lib/statsig/hero-query-overrides.ts diff --git a/src/lib/components/AppwriteIn100Seconds.svelte b/src/lib/components/AppwriteIn100Seconds.svelte index 82774ad90a..3595520784 100644 --- a/src/lib/components/AppwriteIn100Seconds.svelte +++ b/src/lib/components/AppwriteIn100Seconds.svelte @@ -15,7 +15,7 @@ action={trigger} event="intro-video-btn_hero-click" variant="secondary" - class="w-full! cursor-pointer shadow-[0_2px_40px_rgba(0,0,0,0.5)] transition-opacity hover:opacity-90 active:scale-95 lg:w-fit!" + class="w-full! cursor-pointer shadow-[0_2px_40px_rgba(0,0,0,0.5)] transition-opacity hover:opacity-90 active:scale-95 sm:w-fit!" > Appwrite in 100 seconds diff --git a/src/lib/components/homepage-variations/custom-hero.svelte b/src/lib/components/homepage-variations/custom-hero.svelte index 45e0717040..06a369094b 100644 --- a/src/lib/components/homepage-variations/custom-hero.svelte +++ b/src/lib/components/homepage-variations/custom-hero.svelte @@ -1,27 +1,51 @@ -
+
- -

- {title}_ -

-
+ {#if layoutAside || layoutBottomTwoLineTitle} + +

+ {resolved.heroTitle}_ +

+
+ {:else} + +

+ {resolved.heroTitle}_ +

+
+ {/if} -

- {subtitle} +

+ {resolved.heroSubtitle}

-
+
{#if showDashboard} - + {#if layoutAside} + + {:else} +
+ +
+ {/if} {/if}
diff --git a/src/lib/components/homepage-variations/variation-config.ts b/src/lib/components/homepage-variations/variation-config.ts index 95581a34c5..c5378bb45d 100644 --- a/src/lib/components/homepage-variations/variation-config.ts +++ b/src/lib/components/homepage-variations/variation-config.ts @@ -1,7 +1,11 @@ +import type { HeroLayoutVariant } from '$lib/statsig/hero-layout'; + export interface HomepageVariationConfig { title: string; subtitle: string; pageTitle: string; + /** Hero structure: `0` aside dashboard, `1` bottom + wrapped title, `2` bottom + single-line title. */ + heroLayout?: HeroLayoutVariant; slug?: string; ctaLabel?: string; ctaHref?: string; diff --git a/src/lib/statsig/client.ts b/src/lib/statsig/client.ts index c16bcd5e71..7b467ece0c 100644 --- a/src/lib/statsig/client.ts +++ b/src/lib/statsig/client.ts @@ -3,16 +3,19 @@ import { ENV } from '$lib/system'; import { STATSIG_STABLE_ID_KEY, STATSIG_EXPERIMENT_BEST_DESCRIPTION, + STATSIG_EXPERIMENT_HERO_LAYOUT, STATSIG_CLIENT_SDK_KEY } from '$lib/statsig/constants'; -export { STATSIG_EXPERIMENT_BEST_DESCRIPTION }; +export { STATSIG_EXPERIMENT_BEST_DESCRIPTION, STATSIG_EXPERIMENT_HERO_LAYOUT }; /** Narrow surface we use from `@statsig/js-client` (avoids static `import type` from that package, which can break Vite named re-exports). */ export type StatsigBrowserClient = { initializeSync(options?: object): unknown; initializeAsync(options?: object): Promise; - getExperiment(name: string): { get(key: string, defaultValue: string): string }; + getExperiment(name: string): { + get(key: string, defaultValue: string | number): string | number; + }; logEvent(eventOrName: string, value?: string | number, metadata?: Record): void; shutdown(): Promise; }; diff --git a/src/lib/statsig/constants.ts b/src/lib/statsig/constants.ts index 40c5d82670..ffb8834ba0 100644 --- a/src/lib/statsig/constants.ts +++ b/src/lib/statsig/constants.ts @@ -6,5 +6,10 @@ export const STATSIG_CLIENT_SDK_KEY = 'client-TRp4ODQ3Yfsha0XwmRayqwb7WW0ujUbiGr export const STATSIG_EXPERIMENT_BEST_DESCRIPTION = 'best_description'; +/** Experiment param key: `layout` (values `0` | `1` | `2`). */ +export const STATSIG_EXPERIMENT_HERO_LAYOUT = 'hero_layout'; + export const DEFAULT_HERO_SUBTITLE = 'Appwrite is an open-source, all-in-one development platform. Use built-in backend infrastructure and web hosting, all from a single place.'; + +export const DEFAULT_HERO_TITLE = 'Build like a team of hundreds'; diff --git a/src/lib/statsig/hero-layout.ts b/src/lib/statsig/hero-layout.ts new file mode 100644 index 0000000000..30a1d8e574 --- /dev/null +++ b/src/lib/statsig/hero-layout.ts @@ -0,0 +1,13 @@ +/** `hero_layout` experiment: `0` aside dashboard, `1` bottom + multi-line title, `2` bottom + single-line title. */ +export type HeroLayoutVariant = 0 | 1 | 2; + +export function normalizeHeroLayout(raw: unknown, fallback: HeroLayoutVariant): HeroLayoutVariant { + if (typeof raw === 'number' && (raw === 0 || raw === 1 || raw === 2)) { + return raw; + } + const n = parseInt(String(raw).trim(), 10); + if (n === 0 || n === 1 || n === 2) { + return n; + } + return fallback; +} diff --git a/src/lib/statsig/hero-query-overrides.ts b/src/lib/statsig/hero-query-overrides.ts new file mode 100644 index 0000000000..62741ffde6 --- /dev/null +++ b/src/lib/statsig/hero-query-overrides.ts @@ -0,0 +1,67 @@ +import { normalizeHeroLayout, type HeroLayoutVariant } from './hero-layout'; + +/** Query overrides for marketing hero experiments (Statsig parity for local QA). */ + +export const HERO_LAYOUT_QUERY_KEY = 'hero_layout'; +export const HERO_SUBTITLE_QUERY_KEY = 'hero_subtitle'; +export const HERO_TITLE_QUERY_KEY = 'hero_title'; + +const MAX_SUBTITLE_LEN = 560; +const MAX_TITLE_LEN = 160; + +export type HeroQueryBaseline = { + heroLayout: HeroLayoutVariant; + heroSubtitle: string; + heroTitle: string; +}; + +export type HeroQueryResolved = HeroQueryBaseline; + +export function hasHeroExperimentQueryOverrides(params: URLSearchParams): boolean { + return ( + params.has(HERO_LAYOUT_QUERY_KEY) || + params.has(HERO_SUBTITLE_QUERY_KEY) || + params.has(HERO_TITLE_QUERY_KEY) + ); +} + +/** + * Applies `hero_layout`, `hero_subtitle`, and `hero_title` search params when present. + * Values are clamped; unknown `hero_layout` values keep the baseline layout. + */ +export function resolveHeroQueryOverrides( + params: URLSearchParams, + baseline: HeroQueryBaseline +): HeroQueryResolved { + return { + heroLayout: readHeroLayoutOverride(params, baseline.heroLayout), + heroSubtitle: readHeroSubtitleOverride(params, baseline.heroSubtitle), + heroTitle: readHeroTitleOverride(params, baseline.heroTitle) + }; +} + +function readHeroLayoutOverride( + params: URLSearchParams, + fallback: HeroLayoutVariant +): HeroLayoutVariant { + if (!params.has(HERO_LAYOUT_QUERY_KEY)) return fallback; + return normalizeHeroLayout(params.get(HERO_LAYOUT_QUERY_KEY), fallback); +} + +function readHeroSubtitleOverride(params: URLSearchParams, fallback: string): string { + if (!params.has(HERO_SUBTITLE_QUERY_KEY)) return fallback; + const raw = params.get(HERO_SUBTITLE_QUERY_KEY); + if (raw == null) return fallback; + const t = raw.replace(/\s+/g, ' ').trim(); + if (t.length === 0) return fallback; + return t.length > MAX_SUBTITLE_LEN ? t.slice(0, MAX_SUBTITLE_LEN) : t; +} + +function readHeroTitleOverride(params: URLSearchParams, fallback: string): string { + if (!params.has(HERO_TITLE_QUERY_KEY)) return fallback; + const raw = params.get(HERO_TITLE_QUERY_KEY); + if (raw == null) return fallback; + const t = raw.replace(/\s+/g, ' ').trim(); + if (t.length === 0) return fallback; + return t.length > MAX_TITLE_LEN ? t.slice(0, MAX_TITLE_LEN) : t; +} diff --git a/src/lib/statsig/hero-statsig.server.ts b/src/lib/statsig/hero-statsig.server.ts index 1d1b263040..7e7cb3baff 100644 --- a/src/lib/statsig/hero-statsig.server.ts +++ b/src/lib/statsig/hero-statsig.server.ts @@ -5,7 +5,12 @@ import { type StatsigOptions, type StatsigUserArgs } from '@statsig/statsig-node-core'; -import { STATSIG_CLIENT_SDK_KEY, STATSIG_EXPERIMENT_BEST_DESCRIPTION } from './constants'; +import { + STATSIG_CLIENT_SDK_KEY, + STATSIG_EXPERIMENT_BEST_DESCRIPTION, + STATSIG_EXPERIMENT_HERO_LAYOUT +} from './constants'; +import { normalizeHeroLayout, type HeroLayoutVariant } from './hero-layout'; function buildStatsigServerOptions(): StatsigOptions { const explicit = env.STATSIG_ENVIRONMENT?.trim(); @@ -88,6 +93,31 @@ export async function evaluateHeroDescriptionExperiment( } } +/** + * Evaluates `hero_layout` for SSR. Exposure is logged on the client when the hero reads + * `getExperiment(STATSIG_EXPERIMENT_HERO_LAYOUT).get('layout', …)` after `whenStatsigReady()`. + * + * Configure a dynamic config / experiment parameter named `layout` with values `0`, `1`, or `2`. + */ +export async function evaluateHeroLayoutExperiment( + user: StatsigUser | StatsigServerUserInput, + fallback: HeroLayoutVariant +): Promise { + const client = await getStatsigClient(); + if (!client) return fallback; + + try { + const statsigUser = toStatsigUser(user); + const experiment = client.getExperiment(statsigUser, STATSIG_EXPERIMENT_HERO_LAYOUT, { + disableExposureLogging: true + }); + const raw = experiment.get('layout', fallback); + return normalizeHeroLayout(raw, fallback); + } catch { + return fallback; + } +} + /** * JSON payload for `StatsigClient.dataAdapter.setData` + `initializeAsync`, so the JS SDK skips * cache-first experiment checks (Group Assignment Health). Requires `STATSIG_SERVER_SECRET`. diff --git a/src/routes/(marketing)/(components)/dashboard.svelte b/src/routes/(marketing)/(components)/dashboard.svelte index 32da1d5b75..5c5263488a 100644 --- a/src/routes/(marketing)/(components)/dashboard.svelte +++ b/src/routes/(marketing)/(components)/dashboard.svelte @@ -1,5 +1,9 @@ -
+
- + {#if layoutAside} + + {:else} +
+ +
+ {/if} - -

- {title}_ -

-
+ {#if layoutAside || layoutBottomTwoLineTitle} + +

+ {resolved.heroTitle}_ +

+
+ {:else} + +

+ {resolved.heroTitle}_ +

+
+ {/if}

- {subtitle} + {resolved.heroSubtitle}

-
+
- - + {/if}
diff --git a/src/routes/(marketing)/+page.server.ts b/src/routes/(marketing)/+page.server.ts index cb158aad1a..f8fad28f8c 100644 --- a/src/routes/(marketing)/+page.server.ts +++ b/src/routes/(marketing)/+page.server.ts @@ -1,6 +1,12 @@ -import { STATSIG_STABLE_ID_KEY, DEFAULT_HERO_SUBTITLE } from '$lib/statsig/constants'; +import { + STATSIG_STABLE_ID_KEY, + DEFAULT_HERO_SUBTITLE, + DEFAULT_HERO_TITLE +} from '$lib/statsig/constants'; +import { resolveHeroQueryOverrides } from '$lib/statsig/hero-query-overrides'; import { evaluateHeroDescriptionExperiment, + evaluateHeroLayoutExperiment, getStatsigClientBootstrapPayload } from '$lib/statsig/hero-statsig.server'; import type { PageServerLoad } from './$types'; @@ -26,13 +32,22 @@ export const load: PageServerLoad = async ({ cookies, request, url }) => { ...(userAgent ? { userAgent } : {}) }; - const [heroSubtitle, statsigBootstrap] = await Promise.all([ + const [heroSubtitleBase, heroLayoutBase, statsigBootstrap] = await Promise.all([ evaluateHeroDescriptionExperiment(user, DEFAULT_HERO_SUBTITLE), + evaluateHeroLayoutExperiment(user, 0), getStatsigClientBootstrapPayload(user) ]); + const { heroSubtitle, heroLayout, heroTitle } = resolveHeroQueryOverrides(url.searchParams, { + heroLayout: heroLayoutBase, + heroSubtitle: heroSubtitleBase, + heroTitle: DEFAULT_HERO_TITLE + }); + return { heroSubtitle, + heroLayout, + heroTitle, statsigBootstrap, /** Same value as `STATSIG_STABLE_ID_KEY` cookie — pass to client init to avoid bootstrap / stableID mismatch. */ statsigStableUserId: stableId, diff --git a/src/routes/(marketing)/+page.svelte b/src/routes/(marketing)/+page.svelte index f08e9b0ac6..7c951dda7c 100644 --- a/src/routes/(marketing)/+page.svelte +++ b/src/routes/(marketing)/+page.svelte @@ -23,7 +23,7 @@ />
- + diff --git a/src/routes/[variation]/+page.svelte b/src/routes/[variation]/+page.svelte index 23198858b2..95b37b1ef2 100644 --- a/src/routes/[variation]/+page.svelte +++ b/src/routes/[variation]/+page.svelte @@ -27,6 +27,7 @@ showDashboard={config.showDashboard} ctaLabel={config.ctaLabel} ctaHref={config.ctaHref} + heroLayout={config.heroLayout ?? 0} /> {#if config.showPlatforms} From ce1da0c5d167e3092c7e83a845bdc0ab3e179072 Mon Sep 17 00:00:00 2001 From: eldadfux Date: Wed, 29 Apr 2026 06:53:53 -0400 Subject: [PATCH 2/4] format --- .../homepage-variations/custom-hero.svelte | 12 ++++++---- .../(marketing)/(components)/dashboard.svelte | 4 ++-- .../(marketing)/(components)/hero.svelte | 22 ++++++++++--------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/lib/components/homepage-variations/custom-hero.svelte b/src/lib/components/homepage-variations/custom-hero.svelte index 06a369094b..85510a4eef 100644 --- a/src/lib/components/homepage-variations/custom-hero.svelte +++ b/src/lib/components/homepage-variations/custom-hero.svelte @@ -65,7 +65,9 @@
{#if layoutAside || layoutBottomTwoLineTitle} @@ -81,7 +83,7 @@ {:else}

{resolved.heroTitle}_ @@ -92,7 +94,7 @@

{resolved.heroSubtitle} @@ -101,7 +103,9 @@