✅ PART 1 LAUNCHED — 2026-07-22 (
prod-2026-07-22-001)./domain-broker/(English) serves v2 (Minimal hero + FormV2) to bots + humans, indexed, hero A/B ended (HERO_AB_ACTIVE=false, vpn-translatee81136a). Site→prod at 20:17:47Z, worker flag→false at 20:20:38Z. Verified: Googlebot v2, no noindex/X-Robots, self-canonical,/fr/hub stays v1. bowtie note #180. A/B data cut: pre-20:17:47Z D1hero_variantonly (sticky cookie corrupts later rows).⏸ PART 2 PARKED — needs editing
DomainBrokerFormV2.astro(thegs_variantfield), which hits the user's coordination gate (possible collision with the other worker). Awaiting user go + collision sync. (A route that avoids the form edit exists — read theab_…get-startedcookie server-side indomain-intake.js, gated on referer path — surfaced as a proposal.)PART 3 (i18n) — later push, unchanged.
Goal (from July 21 meeting + Ben):
/domain-broker/ (organic hub): promote v2 (the Minimal hero + DomainBrokerFormV2) to 100% of traffic, permanent and indexed. End the hero A/B./domain-broker/get-started/ (paid lander): start a fresh A/B — control = current legacy flat form, variant B = the same DomainBrokerFormV2 treatment.This is the meeting's "v2 → 100% organic, A/B on paid (flip switch)" split, made concrete.
Deliverable mode is spec/doc — this runbook is written for the implementing worker. Nothing here has been executed.
The hero A/B is currently LIVE on prod (vpn-workers/translate/wrangler.toml → HERO_AB_ACTIVE = "true", commit 96107a2). Variant a = current hero, variant b = the Minimal hero-v2 build.
"Turn off the A/B" and "make v2 live" are OPPOSITE switches. Setting HERO_AB_ACTIVE="false" reverts 100% of traffic to variant a — the OLD hero. It does NOT promote v2. Do not just flip the flag.
v2 must be promoted at the page level so bots, organic, and everyone get it, properly indexed. The flag then goes false only after the origin page is already v2.
/domain-broker/The variant-B experience is DomainBrokerPage.astro rendered with heroVariant="b" (swaps DomainBrokerForm → DomainBrokerFormV2; every section below the hero is identical). Backend for FormV2's structured domains[] is already shipped and live (migration 0007, domain-intake.js normalizes both shapes).
Make the real indexed page = v2.
src/pages/domain-broker/index.astro — add the prop:
<DomainBrokerPage lang="en" heroVariant="b" hreflangMap={hreflangMap} />
(Currently no heroVariant, so it defaults to 'a'.) This makes /domain-broker/ itself serve the Minimal hero + FormV2 — crawlable, indexed, canonical intact. <h1> stays "Premium Domain Broker" (both variants already share it), so the recovering ranking is untouched.
Localized hubs — DECIDED: English-only for launch.
index.astrois English-only, andDomainBrokerFormV2is not i18n-ready (see Part 3). Do NOT promote v2 on the translated/{lang}/domain-broker/hubs yet — they stay on control (v1 flat form) until FormV2 is localized. Ship v2 to/domain-broker/(English) only.
Stop the worker from swapping.
vpn-workers/translate/wrangler.toml → HERO_AB_ACTIVE = "false". With the origin now v2, the worker serves origin directly = v2 for everyone including bots. (Do NOT do this step before step 1 is deployed, or you serve the old hero.)
Deploy — order matters.
bash scripts/deploy.sh → staging, verify /domain-broker/ shows the Minimal hero (FormV2, tp-kicker H1 class) with no noindex, then bash scripts/promote.sh → prod.bash vpn-workers/translate/deploy.sh (token-guarded).Verify live (bot + human + index):
# Bot must now see v2 (Minimal), not old hero:
curl -s "https://www.vpn.com/domain-broker/" -A "Googlebot" | grep -oiE '<h1[^>]*>[^<]*</h1>' # expect tp-kicker class
# No noindex on the canonical page:
curl -sI "https://www.vpn.com/domain-broker/" | grep -i x-robots-tag # expect NOTHING
curl -s "https://www.vpn.com/domain-broker/" | grep -i 'name="robots"' # expect index,follow or absent
# Canonical self-referential:
curl -s "https://www.vpn.com/domain-broker/" | grep -i 'rel="canonical"' # → /domain-broker/
Cleanup (follow-up commit, not launch-blocking):
src/pages/domain-broker/hero-v2.astro (it was the noindex clone of v2; now redundant and would be a near-duplicate of the live page).serveHeroAB branch + helpers from vpn-workers/translate/src/index.js (the pathname === '/domain-broker/' block, lines ~96-100 and the serveHeroAB/buildVariantResponse/finalizeAB/abActive helpers) so the worker has no dead A/B path. Keep the redirect + bot ordering intact — that block runs BEFORE the A/B branch, don't disturb it.HERO_AB_ACTIVE="false" keeps it dormant, so cleanup can be its own PR.SEO watch: run /before-and-after (or the before-and-after skill) snapshot on /domain-broker/ at promote time; watch "premium domain broker" position weekly. The verdict data from the killed A/B lives in D1 hero_variant if you want the retro.
Announce: mcp__askbowtie__add_note — category deploy, domain vpn.com: "Domain broker v2 (Minimal hero + FormV2) promoted to 100% on /domain-broker/; hero A/B ended (HERO_AB_ACTIVE=false)."
✅ PART 2 LAUNCHED — 2026-07-22 21:52:22Z. Site→prod
prod-2026-07-22-002; workerGETSTARTED_AB_ACTIVE="true"(vpn-translate deployed). Testdomain-broker-get-started-2026live on/domain-broker/get-started/: a=legacy flatDomainBrokerForm, b=DomainBrokerFormV2, 50/50 sticky, bots→control. Attribution proven end-to-end (gs=a/gs=b tagged; hub lead w/ get-started cookie → gs=NULL, no pollution; hero regression clean). Prod bucketing verified 50/50 + sticky. bowtie note #182; exposure flowsget-started-a(#47) /get-started-b(#48). Judge from D1gs_variant(SELECT gs_variant, COUNT(*) FROM domain_intake WHERE gs_variant IN ('a','b') GROUP BY gs_variant). Kill switch:GETSTARTED_AB_ACTIVE="false"+ redeploy worker. Cleanup: delete 3 GHL test contactspeetermans.b+gsattrib{a,b,hub}@gmail.com(D1 rows already removed).
/domain-broker/get-started/ (src/pages/domain-broker/get-started.astro) is the paid lander — no header, form-first, uses the legacy flat DomainBrokerForm. Test whether the structured DomainBrokerFormV2 (the v2 treatment) converts better here too.
domain-broker-get-started-2026 (convention {page}-{element}-{year}).DomainBrokerForm).DomainBrokerFormV2 in the hero, everything else identical.This mirrors the hero-A/B mechanism exactly — reuse it, don't reinvent. Same worker, same caching model, same bot→control rule.
Variant-B build page — src/pages/domain-broker/get-started-v2.astro:
get-started.astro, swap <DomainBrokerForm /> (line 57) → <DomainBrokerFormV2 lang="en" />.noindex + canonicalUrl → /domain-broker/get-started/ on the Base layout (same pattern as hero-v2.astro).hero-v2 is excluded).Worker branch — vpn-workers/translate/src/index.js:
if (pathname === '/domain-broker/get-started/') → serveGetStartedAB(...), placed after the redirect lookup + bot pass-through (same position as the hero branch — see [[project_translate_bot_redirect_order]], redirect lookup MUST run first).variant b fetches /domain-broker/get-started-v2/). New flag GETSTARTED_AB_ACTIVE in wrangler.toml (start "false", flip to "true" to launch; staging always-on via the same host-aware abActive pattern).?__abcache_gs=<v>) so the two tests never share a cached body.Attribution — do NOT pollute the hero arm.
DomainBrokerForm and DomainBrokerFormV2 are shared across /domain-broker/ and /domain-broker/get-started/. Per the Fable note in the hero plan, a hidden variant field must be set only when its own cookie exists AND the path matches — never defaulted, or every get-started/bot/no-cookie lead dumps into an arm.
gs_variant), gated on ab_domain-broker-get-started-2026 cookie AND path /domain-broker/get-started/. Missing = null, never 'a'./api/domain-intake (vpn-workers/forms/src/domain-intake.js): store gs_variant in a new D1 column on domain_intake (mirror the hero_variant column from migration 0007). Add it to the domain_inquiry payload.gs_variant (definitive count). itbroke ab_variant events = exposure denominator only. Do not chain browser→server events through a bowtie flow (synthetic session, won't chain — our gateway finding).Launch:
?v=b shows FormV2, ?v=a shows the flat form, bots get control, no noindex leak on the real URL. Promote.GETSTARTED_AB_ACTIVE="true" + bash vpn-workers/translate/deploy.sh.get-started-a / get-started-b = ab_variant(test=domain-broker-get-started-2026, variant=a/b) → domain_inquiry(gs_variant=a/b). Trend-only.add_note on launch.GETSTARTED_AB_ACTIVE="false" + redeploy = instant full revert (cookied users too), same honest-kill semantics as the hero flag. Hard kill = delete get-started-v2.astro.
DomainBrokerFormV2 i18n-ready (AFTER Parts 1 & 2 are live)Ben: "once that is in place, we need to prep the v2 form to be i18n ready." This unlocks promoting v2 on the translated /{lang}/domain-broker/ hubs later.
Current state (audited): DomainBrokerFormV2.astro (2,485 lines) takes a lang prop but has zero t()/tx()/resolveUrl() calls — it is fully hardcoded English. This is real work, not a cleanup pass.
Scope:
src/i18n/translations.ts as a named export (e.g. domainBrokerFormV2), keyed across all 16 languages. Follow the New Component pattern in CLAUDE.md.<script> must be passed in via a <script type="application/json"> config block (the PROJECT.md client-side i18n pattern), not hardcoded.resolveUrl() / localizeUrl().translations.ts is a contamination-landmine SSOT — pull up LayerView before editing (lv describe file:src/i18n/translations.ts vpn-astro), record the change after (lv remember file:src/i18n/translations.ts -s changes "…" vpn-astro), and run node scripts/i18n-contamination-check.mjs --gate + node scripts/translation-qa.mjs after build. The build gate fails the deploy otherwise.scripts/translate-ui-strings.mjs), then promote v2 on the localized hubs by passing heroVariant="b" through the [lang]/domain-broker/ routes.This is a separate push (likely Tuesday+), gated on Parts 1-2 being live and verified.
/domain-broker/ (English)./domain-broker/get-started/ (control = flat form, B = FormV2).Part 2 + Part 3 both touch DomainBrokerFormV2 (and Part 3 touches translations.ts) — the same files the other worker may be in for provider/flow work. Sync before starting to avoid collision.
Part 1 is small and Friday-ready: flip index.astro to heroVariant="b", deploy site→prod, then set HERO_AB_ACTIVE="false" and deploy the worker — in that order. Do not flip the flag alone; that ships the old hero. Part 2 clones the proven hero-A/B mechanism for the paid lander with strict per-test attribution gating, judged from a new D1 gs_variant column.