diff --git a/src/components/home/hero-v76.tsx b/src/components/home/hero-v76.tsx index cb88ed8..c859bc4 100644 --- a/src/components/home/hero-v76.tsx +++ b/src/components/home/hero-v76.tsx @@ -173,9 +173,9 @@ export function HeroV76({ }) { const carousel = carouselFrom(templates); - // Every render carries one of the three messages now. Without a variant the - // visitor is simply outside the test — they are not shown the old headline, - // which is retired, but the fallback message on the fallback layout. + // Every render carries the shipped message. Without a variant the visitor is + // simply outside the arm — they are not shown the old headline, which is + // retired, but the fallback message on the fallback layout. const message = COPY_VARIANTS[variant?.copy ?? FALLBACK_COPY]; const layout = variant?.layout ?? FALLBACK_LAYOUT; // A message uses its own lockup where it sets one, and balances on its own @@ -437,7 +437,7 @@ export function HeroV76({ )} - {layout === "v2" ? ( + {layout === "v3" ? ( -v2`. New names keep Experiment 2 signups from pooling with Experiment - * 1's in the readout. + * It started as a test of three messages crossed with two layouts (`control`, + * the composer, and `bigtype`). Big type won the first read and became the only + * layout, and the arms were renamed `-v2` when the page below the hero + * changed. The second read ranked the client experience message first, so + * Round 3 stops the test and ships that message to everyone, with a rewritten + * body, as `clients-v3`. The other two messages are in git history. + * + * The arm machinery stays on purpose. Signups still carry `heroArm`, so the + * shipped hero has a baseline of its own, and the new name keeps it from + * pooling with Round 2's readout. A later round can add arms back here without + * touching the middleware. * * Keys are semantic rather than lettered on purpose. The prototype's switcher * letters its layouts by list position while the URL keys them by their own @@ -17,15 +24,15 @@ * this test has no traffic to read. */ -/** The message. */ -export const COPY_KEYS = ["firm", "builder", "clients"] as const; +/** The message. Only the one that won Round 2 ships. */ +export const COPY_KEYS = ["clients"] as const; export type CopyKey = (typeof COPY_KEYS)[number]; -/** The page version. `v2` is big type over the Experiment 2 page. */ -export const LAYOUT_KEYS = ["v2"] as const; +/** The page version. `v3` is big type with the Round 3 body copy. */ +export const LAYOUT_KEYS = ["v3"] as const; export type LayoutKey = (typeof LAYOUT_KEYS)[number]; -/** `-`, e.g. "firm-v2". What the cookie and events carry. */ +/** `-`, e.g. "clients-v3". What the cookie and events carry. */ export type Arm = `${CopyKey}-${LayoutKey}`; export const ARMS: Arm[] = COPY_KEYS.flatMap((copy) => @@ -33,21 +40,12 @@ export const ARMS: Arm[] = COPY_KEYS.flatMap((copy) => ); /** - * What renders for anyone the test leaves out — logged-in visitors, and every - * request while the kill switch is on. - * - * A test message on the shipped layout, NOT the headline the site used to - * carry. "The platform firms run on and build on" is retired: the three - * messages below replace it outright, and a visitor outside the test should see - * the site as it now reads rather than a headline nothing else still uses. - * - * The cost of that is deliberate and worth naming: there is no holdout. No arm - * and no fallback reproduces the old page, so the test can say which message - * and which layout win against each other, and cannot say whether any of them - * beats what came before. + * What renders for anyone the arm leaves out — logged-in visitors, and every + * request while the kill switch is on. It is the shipped hero, so a visitor + * outside the arm sees the same page as everyone else. */ -export const FALLBACK_COPY: CopyKey = "firm"; -export const FALLBACK_LAYOUT: LayoutKey = "v2"; +export const FALLBACK_COPY: CopyKey = "clients"; +export const FALLBACK_LAYOUT: LayoutKey = "v3"; export interface CopyVariant { key: CopyKey; @@ -76,54 +74,6 @@ export interface CopyVariant { } export const COPY_VARIANTS: Record = { - firm: { - key: "firm", - label: "AI-native firm", - h1: "Become an AI-native business", - // Breaks before the compound rather than after it. Left to balance itself - // this came out "Become an AI-native" / "business", which leaves a single - // word on the second line and splits the phrase that carries the message. - h1Lines: ["Become an", "AI-native business"], - // Re-anchored with the headline. These are matched by `indexOf`, so the - // previous anchors ("firm", "for") would simply not be found in the new - // wording and the big-type arm would have rendered with no app tiles at - // all — silently, since a tile that isn't located is dropped. Same count - // and rhythm as before: a pair early, a single before the last word. - big: { - // Hand-set, because the automatic measure breaks "AI-native" at its own - // hyphen — the 16ch cap lands mid-compound at every width, and a split - // hyphenated word at display size reads as a mistake. - lines: ["Become", "an AI-native business"], - iconAfter: "Become", - iconPair: true, - iconBefore: "business", - }, - body: "Start with customizable apps for client intake, proposals, payments, project tracking, and more. Describe anything else, and AI builds it.", - }, - builder: { - key: "builder", - label: "Builder for real businesses", - h1: "The AI app builder for real businesses, not weekend projects", - h1Lines: [ - "The AI app builder for real", - "businesses, not weekend projects", - ], - big: { - // The comma is the one in `h1`. The prototype's lockup dropped it, which - // would have made this message read differently in the two layouts — and - // a message is supposed to be the same thing in both, or the layout read - // is picking up a copy change. - lines: [ - "The AI app builder", - "for real businesses, not", - "weekend projects", - ], - iconAfter: "real", - iconPair: true, - iconBefore: "projects", - }, - body: "Built apps with secure client logins, permissions, and per-client personalization. Ready to use with clients from day one. No code, no hosting, no auth to wire up.", - }, clients: { key: "clients", label: "Client experience", @@ -133,7 +83,10 @@ export const COPY_VARIANTS: Record = { iconAfter: "Deliver", iconPair: true, }, - body: "Assembly comes with a CRM, a branded client experience, and 30+ apps for intake, proposals, payments, project tracking, and more. Tailor any of them with AI, or build what's missing.", + // Opens on the client and walks their journey in order (onboard, sign, + // collaborate, pay). "Customizable" keeps the point that the ready-made apps + // can be changed, since visitors who miss it try to rebuild them with AI. + body: "Let clients onboard, sign, collaborate, and pay in one branded client experience, with a CRM behind it for your team. Start with 30+ customizable apps, then use AI to build what's missing.", }, }; @@ -154,19 +107,24 @@ export function parseArm( return { copy, layout }; } -/** Experiment 1's layouts, both retired in favour of `v2`. */ -const LEGACY_LAYOUTS = new Set(["control", "bigtype"]); +/** The one arm that ships. */ +const SHIPPED_ARM: Arm = `${FALLBACK_COPY}-${FALLBACK_LAYOUT}`; + +/** Every message and page version from Rounds 1 and 2, all retired. */ +const LEGACY_COPY = new Set(["firm", "builder", "clients"]); +const LEGACY_LAYOUTS = new Set(["control", "bigtype", "v2"]); /** - * The arm a held cookie should become. A visitor holding an Experiment 1 arm - * (`-control` or `-bigtype`) keeps their message and moves to its - * v2 arm, rather than being redrawn onto a message they were never shown. + * The arm a held cookie should become. Any arm from Rounds 1 or 2 moves to the + * shipped one, since its message or page no longer exists, and the rewritten + * cookie means the visitor's next signup reports the hero they actually saw. */ export function migrateArm(value: string | undefined | null): Arm | null { if (!value) return null; + if (parseArm(value)) return value as Arm; const [copy, layout] = value.split("-"); - if (LEGACY_LAYOUTS.has(layout) && isCopyKey(copy)) return `${copy}-v2`; - return parseArm(value) ? (value as Arm) : null; + if (LEGACY_COPY.has(copy) && LEGACY_LAYOUTS.has(layout)) return SHIPPED_ARM; + return null; } /** @@ -175,17 +133,21 @@ export function migrateArm(value: string | undefined | null): Arm | null { * and the same query on this site land on the same arm. */ const COPY_ALIASES: Record = { - a: "firm", - b: "builder", + // Links to the retired messages land on the shipped one rather than failing. + a: "clients", + b: "clients", c: "clients", + firm: "clients", + builder: "clients", }; const LAYOUT_ALIASES: Record = { - // The retired composer layout resolves to big type, so old review links - // still land on their message. - b: "v2", - c: "v2", - control: "v2", - bigtype: "v2", + // Every retired layout and page version resolves to the shipped one, so old + // review links still land on the homepage as it now reads. + b: "v3", + c: "v3", + control: "v3", + bigtype: "v3", + v2: "v3", }; /** @@ -204,7 +166,7 @@ export function armFromQuery(params: URLSearchParams): Arm | null { return `${copy}-${layout}`; } -/** An even draw across the arms. */ +/** An even draw across the arms. With one arm it always returns it. */ export function randomArm(): Arm { return ARMS[Math.floor(Math.random() * ARMS.length)]; }