Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/lib/components/IsLoggedIn.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts">
import { browser } from '$app/environment';
import { page } from '$app/state';
import { Button } from '$lib/components/ui';
import { cn } from '$lib/utils/cn';
import { DEFAULT_HERO_CTA } from '$lib/statsig/constants';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';

interface Props {
Expand All @@ -11,6 +13,11 @@

const { class: className, offerButton = false }: Props = $props();
const isLoggedIn = browser && 'loggedIn' in document.body.dataset;

/** Marketing homepage `best_cta` SSR value; other routes fall back to default. */
const navCtaLabel = $derived(
(page.data as { heroCta?: string | null }).heroCta ?? DEFAULT_HERO_CTA
);
</script>

<Button
Comment on lines 21 to 23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Repeated inline page.data type cast across 8+ components

The pattern (page.data as { heroCta?: string | null }).heroCta ?? DEFAULT_HERO_CTA is copied verbatim into IsLoggedIn.svelte, PreFooter.svelte, site-header.svelte, pricing.svelte (marketing), pricing/+page.svelte, compare-plans.svelte, call-to-action.svelte, and Post.svelte. A single shared type or a tiny helper (e.g. getPageHeroCta(page)) would make the intent clear and prevent one component from silently drifting if the field is ever renamed.

Expand All @@ -25,7 +32,7 @@
>Go to Console</span
>
<span class="block group-[&[data-logged-in]]/body:hidden" aria-hidden={isLoggedIn}
>Start building for free</span
>{navCtaLabel}</span
>
{/if}
</Button>
12 changes: 10 additions & 2 deletions src/lib/components/PreFooter.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
<script lang="ts">
import { page } from '$app/state';
import { Button, type Variant } from '$lib/components/ui';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
import { SHOW_SCALE_PLAN } from '$lib/constants/feature-flags';
import { heroCtaIfShortStartBuilding } from '$lib/statsig/hero-query-overrides';
import { DEFAULT_HERO_CTA } from '$lib/statsig/constants';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';

const navCtaLabel = $derived(
(page.data as { heroCta?: string | null }).heroCta ?? DEFAULT_HERO_CTA
);

const plans: Array<{
name: string;
Expand Down Expand Up @@ -124,7 +131,8 @@
variant={plan.buttonVariant}
class="w-full! flex-3 self-end md:w-fit"
>
<span class="text" style:padding-inline="0.5rem">{plan.buttonText}</span
<span class="text" style:padding-inline="0.5rem"
>{heroCtaIfShortStartBuilding(plan.buttonText, navCtaLabel)}</span
>
</Button>
</div>
Expand Down
31 changes: 22 additions & 9 deletions src/lib/components/homepage-variations/custom-hero.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
import { onMount } from 'svelte';
import { trackEvent } from '$lib/actions/analytics';
import { DEFAULT_HERO_SUBTITLE, DEFAULT_HERO_TITLE } from '$lib/statsig/constants';
import {
DEFAULT_HERO_CTA,
DEFAULT_HERO_SUBTITLE,
DEFAULT_HERO_TITLE
} from '$lib/statsig/constants';
import { initStatsig, whenStatsigReady } from '$lib/statsig/client';
import { readMarketingHeroExperimentsForExposure } from '$lib/statsig/experiments/marketing-hero-client';
import {
Expand All @@ -22,7 +26,7 @@
title = DEFAULT_HERO_TITLE,
subtitle = DEFAULT_HERO_SUBTITLE,
showDashboard = true,
ctaLabel = 'Start building for free',
ctaLabel = DEFAULT_HERO_CTA,
ctaHref = PUBLIC_APPWRITE_DASHBOARD,
heroLayout = 0
} = $props<{
Expand All @@ -37,12 +41,14 @@
/** Same client hydration path as marketing `hero.svelte` (Statsig + query overrides). */
let clientHeroLayout = $state<HeroLayoutVariant | undefined>(undefined);
let clientHeroSubtitle = $state<string | undefined>(undefined);
let clientHeroCta = $state<string | undefined>(undefined);

const resolved = $derived(
resolveHeroQueryOverrides(building ? new URLSearchParams() : page.url.searchParams, {
heroLayout: clientHeroLayout ?? heroLayout,
heroSubtitle: clientHeroSubtitle ?? subtitle,
heroTitle: title
heroTitle: title,
heroCta: clientHeroCta ?? ctaLabel
})
);

Expand All @@ -64,17 +70,24 @@
void whenStatsigReady().then((client) => {
if (!client) return;

const { heroSubtitle: nextSubtitle, heroLayout: nextLayout } =
readMarketingHeroExperimentsForExposure(client, {
heroSubtitle: subtitle,
heroLayout
});
const {
heroSubtitle: nextSubtitle,
heroLayout: nextLayout,
heroCta: nextCta
} = readMarketingHeroExperimentsForExposure(client, {
heroSubtitle: subtitle,
heroLayout,
heroCta: ctaLabel
});
if (nextSubtitle !== subtitle) {
clientHeroSubtitle = nextSubtitle;
}
if (nextLayout !== heroLayout) {
clientHeroLayout = nextLayout;
}
if (nextCta !== ctaLabel) {
clientHeroCta = nextCta;
}
});
});

Expand Down Expand Up @@ -170,7 +183,7 @@
class={layoutAside ? 'w-full! lg:w-fit!' : 'w-full! sm:w-fit!'}
onclick={() => {
trackEvent(`main-get_started_btn_hero-click`);
}}>{ctaLabel}</Button
}}>{resolved.heroCta}</Button
>
</div>
</div>
Expand Down
8 changes: 7 additions & 1 deletion src/lib/components/layout/site-header.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<script lang="ts">
import { page } from '$app/state';
import { DEFAULT_HERO_CTA } from '$lib/statsig/constants';
import { Button } from '../ui';
import HamburgerMenu from './navigation/hamburger-menu.svelte';
import PrimaryNav from './navigation/primary-nav.svelte';

const navCtaLabel = $derived(
(page.data as { heroCta?: string | null }).heroCta ?? DEFAULT_HERO_CTA
);
</script>

<header
Expand All @@ -19,7 +25,7 @@
>

<PrimaryNav class="hidden md:block" />
<Button class="hidden! md:flex!">Start building for free</Button>
<Button class="hidden! md:flex!">{navCtaLabel}</Button>

<HamburgerMenu class="block md:hidden" />
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/lib/layouts/Main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import { Button, Icon, InlineTag } from '$lib/components/ui';
import MongoPartnershipBanner from '$lib/components/MongoPartnershipBanner.svelte';
import { changelogNavBadgeVisible } from '$routes/changelog/utils';
import { DEFAULT_HERO_CTA } from '$lib/statsig/constants';

export let omitMainId = false;
export let hideNavigation = false;
Expand Down Expand Up @@ -157,11 +158,12 @@

$: isOfferPage = page.route.id?.includes('/offer-300') ?? false;

$: navCtaLabel = (page.data as { heroCta?: string | null }).heroCta ?? DEFAULT_HERO_CTA;
$: mobileButtonHref = isOfferPage ? 'https://apwr.dev/DCMWDSw' : getAppwriteDashboardUrl();
$: mobileButtonEvent = isOfferPage
? 'mobile-claim_300_credits_btn-click'
: 'main-start_building_btn-click';
$: mobileButtonText = isOfferPage ? 'Claim 300$ credits' : 'Start building';
$: mobileButtonText = isOfferPage ? 'Claim 300$ credits' : navCtaLabel;

const handleNav = () => {
$isMobileNavOpen = !$isMobileNavOpen;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/statsig/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ 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';

export const DEFAULT_HERO_CTA = 'Start building for free';
16 changes: 15 additions & 1 deletion src/lib/statsig/experiments/marketing-hero-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
import type { StatsigBrowserClient } from '../client';
import { readLayoutVariantFromStatsigClient } from '../experiment-eval';
import type { HeroLayoutVariant } from '../hero-layout';
import { normalizeHeroSubtitle } from '../hero-query-overrides';
import { normalizeHeroCta, normalizeHeroSubtitle } from '../hero-query-overrides';
import { MARKETING_HERO_EXPERIMENTS } from './marketing-hero-ids';

export { MARKETING_HERO_EXPERIMENTS };

export type MarketingHeroStatsigBaseline = {
heroSubtitle: string;
heroLayout: HeroLayoutVariant;
heroCta: string;
};

export type MarketingHeroClientExposureDebug = Record<string, unknown>;
Expand All @@ -29,30 +30,43 @@ export function readMarketingHeroExperimentsForExposure(
): {
heroSubtitle: string;
heroLayout: HeroLayoutVariant;
heroCta: string;
debug: MarketingHeroClientExposureDebug;
} {
const rawDescription = client
.getExperiment(MARKETING_HERO_EXPERIMENTS.bestDescription)
.get('description', baseline.heroSubtitle);

const rawCta = client
.getExperiment(MARKETING_HERO_EXPERIMENTS.bestCta)
.get('cta', baseline.heroCta);

const layoutRead = readLayoutVariantFromStatsigClient(
client,
MARKETING_HERO_EXPERIMENTS.heroLayout,
baseline.heroLayout
);

const heroSubtitle = normalizeHeroSubtitle(rawDescription, baseline.heroSubtitle);
const heroCta = normalizeHeroCta(rawCta, baseline.heroCta);

return {
heroSubtitle,
heroLayout: layoutRead.layout,
heroCta,
debug: {
[MARKETING_HERO_EXPERIMENTS.bestDescription]: {
raw: rawDescription,
rawType: typeof rawDescription,
normalizedLen: heroSubtitle.length,
ssrBaselineLen: baseline.heroSubtitle.length
},
[MARKETING_HERO_EXPERIMENTS.bestCta]: {
raw: rawCta,
rawType: typeof rawCta,
normalizedLen: heroCta.length,
ssrBaselineLen: baseline.heroCta.length
},
[MARKETING_HERO_EXPERIMENTS.heroLayout]: {
normalized: layoutRead.layout,
readSource: layoutRead.source,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/statsig/experiments/marketing-hero-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
export const MARKETING_HERO_EXPERIMENTS = {
/** Param: `description` (string). */
bestDescription: 'best_description',
/** Param: `cta` (string). Primary hero dashboard button label. */
bestCta: 'best_cta',
/**
* Experiment or dynamic config id. Params tried: `layout`, `hero_layout`, `heroLayout`; then `.value`.
*/
Expand Down
41 changes: 38 additions & 3 deletions src/lib/statsig/experiments/marketing-hero-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import type { Statsig, StatsigUser } from '@statsig/statsig-node-core';
import { readLayoutVariantFromStatsigEvaluation } from '../experiment-eval';
import type { HeroLayoutVariant } from '../hero-layout';
import { normalizeHeroSubtitle } from '../hero-query-overrides';
import { normalizeHeroCta, normalizeHeroSubtitle } from '../hero-query-overrides';
import {
getStatsigClientBootstrapPayloadForClient,
getStatsigServerClient,
Expand All @@ -23,6 +23,7 @@ const DISABLE_EXPOSURE = { disableExposureLogging: true } as const;
export type MarketingHomeStatsigBundle = {
heroSubtitleBase: string;
heroLayoutBase: HeroLayoutVariant;
heroCtaBase: string;
statsigBootstrap: string | null;
};

Expand Down Expand Up @@ -86,6 +87,24 @@ async function evaluateHeroDescriptionWithClient(
}
}

async function evaluateHeroCtaWithClient(
client: Statsig,
statsigUser: StatsigUser,
fallback: string
): Promise<string> {
try {
const experiment = client.getExperiment(
statsigUser,
MARKETING_HERO_EXPERIMENTS.bestCta,
DISABLE_EXPOSURE
);
const raw = experiment.get('cta', fallback);
return normalizeHeroCta(raw, fallback);
} catch {
return fallback;
}
}

async function evaluateHeroLayoutWithClient(
client: Statsig,
statsigUser: StatsigUser,
Expand Down Expand Up @@ -113,7 +132,7 @@ async function evaluateHeroLayoutWithClient(
export async function loadMarketingHomeStatsigBundle(
user: StatsigUser | StatsigServerUserInput,
cacheKey: string,
fallbacks: { subtitle: string; layout: HeroLayoutVariant }
fallbacks: { subtitle: string; layout: HeroLayoutVariant; cta: string }
): Promise<MarketingHomeStatsigBundle> {
const now = Date.now();
sweepExpiredMarketingHomeStatsigCache(now);
Expand All @@ -129,6 +148,7 @@ export async function loadMarketingHomeStatsigBundle(
return {
heroSubtitleBase: fallbacks.subtitle,
heroLayoutBase: fallbacks.layout,
heroCtaBase: fallbacks.cta,
statsigBootstrap: null
};
}
Expand All @@ -138,14 +158,16 @@ export async function loadMarketingHomeStatsigBundle(
const bootstrapFilters = {
experimentFilter: new Set([
MARKETING_HERO_EXPERIMENTS.bestDescription,
MARKETING_HERO_EXPERIMENTS.bestCta,
MARKETING_HERO_EXPERIMENTS.heroLayout
]),
dynamicConfigFilter: new Set([MARKETING_HERO_EXPERIMENTS.heroLayout])
};

const [heroSubtitleBase, heroLayoutBase, statsigBootstrap] = await Promise.all([
const [heroSubtitleBase, heroLayoutBase, heroCtaBase, statsigBootstrap] = await Promise.all([
evaluateHeroDescriptionWithClient(client, statsigUser, fallbacks.subtitle),
evaluateHeroLayoutWithClient(client, statsigUser, fallbacks.layout),
evaluateHeroCtaWithClient(client, statsigUser, fallbacks.cta),
Promise.resolve(
getStatsigClientBootstrapPayloadForClient(client, statsigUser, bootstrapFilters)
)
Expand All @@ -154,6 +176,7 @@ export async function loadMarketingHomeStatsigBundle(
const bundle: MarketingHomeStatsigBundle = {
heroSubtitleBase,
heroLayoutBase,
heroCtaBase,
statsigBootstrap
};

Expand Down Expand Up @@ -190,3 +213,15 @@ export async function evaluateHeroLayoutExperiment(
if (!client) return fallback;
return evaluateHeroLayoutWithClient(client, toStatsigUser(user), fallback);
}

/**
* SSR: `best_cta` (`cta` param). Client must still call `readMarketingHeroExperimentsForExposure` so Pulse gets an exposure.
*/
export async function evaluateHeroCtaExperiment(
user: StatsigUser | StatsigServerUserInput,
fallback: string
): Promise<string> {
const client = await getStatsigServerClient();
if (!client) return fallback;
return evaluateHeroCtaWithClient(client, toStatsigUser(user), fallback);
}
Loading
Loading