Skip to content

Commit 8e4e7cd

Browse files
fix(marketing): Team CTA contact-sales (not buyable) + hero self-serve reframe (#214)
Launch-readiness polish for the public homepage (instanode-web). Display-detail accuracy (HARD pillar) — fix a real Team-shown-as-buyable bug: - The homepage Team tile's CTA pointed at the self-serve /app/checkout?plan=team path. Team is gated (TEAM-GATE, 2026-06-04 CEO directive): the server-side gate rejects a Team checkout with 400 tier_not_yet_available, so the homepage was marketing a tier the platform can't sell. The inline comment already flagged this as a known reconciliation follow-up. - Reconciled with PricingPage.tsx + BillingPage.tsx: Team CTA is now "Contact sales →" → mailto:sales@instanode.dev (named const SALES_MAILTO_TEAM). No surface markets Team as buyable. Pro remains the only "Most popular"-badged tier; Team is not badged/highlighted. - Verified against live rendered output: instanode.dev currently serves the buyable Team CTA; the freshly-built dist now serves the contact-sales mailto. Marketing copy polish (safe, non-fabricated): - Hero subhead reframed to emphasize self-serve reliability: "Self-serve from the first call to a paid plan — no signup, no Docker, no sales call." (Hobby/Pro are genuinely self-serve checkout; no invented claims.) NO fabricated social proof added (no invented counts/logos/testimonials). NO Team marketing as available/buyable/Most-Popular. Regression guards: two new MarketingPage.test.tsx tests pin the Team CTA to a contact-sales mailto (forbidding plan=team / "Start team" from creeping back) and assert exactly one "Most popular" badge, on Pro. npm run gate green (tsc + build + 1149 vitest, +2 new). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c031fce commit 8e4e7cd

2 files changed

Lines changed: 57 additions & 9 deletions

File tree

src/pages/MarketingPage.test.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,46 @@ describe('MarketingPage — claim consistency (T18 P1-4 / P1-6)', () => {
121121
expect(text).toMatch(/~60s/)
122122
})
123123

124+
// TEAM-GATE reconciliation (2026-06-08): the homepage Team tile's CTA used
125+
// to point at the self-serve /app/checkout?plan=team path — a tier the
126+
// server-side gate rejects with 400 tier_not_yet_available, so the homepage
127+
// was marketing a plan the platform can't actually sell. Team is
128+
// contact-sales only until its delivery is proven built (CEO TEAM-GATE
129+
// directive). These guards pin the homepage Team CTA to a contact-sales
130+
// mailto and forbid the self-serve checkout link from creeping back, AND
131+
// confirm Team is NOT badged "Most popular" (Pro is the highlighted tier).
132+
it('homepage Team tile CTA is a contact-sales mailto, NOT a self-serve checkout', () => {
133+
const { container } = render(
134+
<MemoryRouter initialEntries={['/']}>
135+
<MarketingPage />
136+
</MemoryRouter>,
137+
)
138+
const teamCta = findAnchorByText('Contact sales →')
139+
expect(teamCta).not.toBeNull()
140+
expect(teamCta!.getAttribute('href')).toContain('mailto:sales@instanode.dev')
141+
// The retired self-serve Team checkout link must not exist anywhere on the page.
142+
const hrefs = Array.from(container.querySelectorAll('a')).map((a) => a.getAttribute('href') ?? '')
143+
expect(hrefs.some((h) => h.includes('plan=team'))).toBe(false)
144+
// And the old buyable label must be gone.
145+
expect(findAnchorByText('Start team →')).toBeNull()
146+
})
147+
148+
it('only Pro is badged "Most popular" — Team must not be highlighted/badged as buyable', () => {
149+
const { container } = render(
150+
<MemoryRouter initialEntries={['/']}>
151+
<MarketingPage />
152+
</MemoryRouter>,
153+
)
154+
const badges = Array.from(container.querySelectorAll('.mkt-featured-flag')).filter(
155+
(el) => (el.textContent ?? '').trim() === 'Most popular',
156+
)
157+
expect(badges.length).toBe(1)
158+
// The badge sits inside the Pro card, not the Team card.
159+
const card = badges[0].closest('.mkt-price-card')
160+
expect(card?.textContent).toContain('Pro')
161+
expect(card?.textContent).not.toContain('Team')
162+
})
163+
124164
// BIZ-3 (2026-05-29): the landing pricing tile shipped "1 small deployment"
125165
// and "10 medium deployments" copy from the days when /deploy/new had a
126166
// deployment_size field. The backend dropped that field; marketing

src/pages/MarketingPage.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ const ROUTES = {
9494
playground: '#playground',
9595
} as const
9696

97+
// Team is sales-assisted only (TEAM-GATE, 2026-06-04 CEO directive): it is NOT
98+
// self-serve and must not route to /app/checkout until its dedicated-infra
99+
// delivery is proven built — the server-side gate rejects a Team checkout with
100+
// 400 tier_not_yet_available. The Team CTA points here, matching the
101+
// contact-sales action PricingPage.tsx and BillingPage.tsx already use.
102+
const SALES_MAILTO_TEAM = 'mailto:sales@instanode.dev?subject=Team%20plan%20enquiry'
103+
97104
type Service = {
98105
id: 'pg' | 'rd' | 'mg' | 'vc' | 'qu' | 'st' | 'wh' | 'dp'
99106
name: string
@@ -203,12 +210,13 @@ const PLANS: Plan[] = [
203210
// Team tier — $199/mo. strict-80% margin redesign (2026-06-05): every
204211
// Team limit is now a finite plans.yaml cap (was -1/unlimited). Above
205212
// these caps = Enterprise (contact sales).
206-
// NOTE (pre-existing, out of scope here): this CTA still points at the
207-
// self-serve /app/checkout?plan=team path, which the server-side Team
208-
// gate rejects with 400 tier_not_yet_available. The PricingPage Team CTA
209-
// already uses a contact-sales mailto. This MarketingPage CTA should be
210-
// reconciled with the gate in a follow-up (not changed here — the
211-
// strict-margin task is explicitly copy-only and must not touch the gate).
213+
// TEAM-GATE reconciliation (2026-06-08): the CTA previously pointed at the
214+
// self-serve /app/checkout?plan=team path, which the server-side Team gate
215+
// rejects with 400 tier_not_yet_available — so the homepage was offering a
216+
// tier the platform can't sell. Now reconciled with PricingPage.tsx +
217+
// BillingPage.tsx: Team is contact-sales only until its delivery is proven
218+
// built. Do NOT re-point this at /app/checkout. Ref TEAM-GATE directive
219+
// (2026-06-04) + docs/sessions/2026-06-04/TEAM-PLAN-GATE-AND-BUILD.md.
212220
id: 'team',
213221
name: 'Team',
214222
tagline: 'For the engineering org. Dedicated infra, RBAC + audit, with SSO & SLA on the way.',
@@ -221,8 +229,8 @@ const PLANS: Plan[] = [
221229
'Need more? Enterprise — contact sales',
222230
],
223231
cta: {
224-
label: 'Start team →',
225-
href: '/app/checkout?plan=team&frequency=monthly',
232+
label: 'Contact sales →',
233+
href: SALES_MAILTO_TEAM,
226234
variant: 'secondary',
227235
},
228236
},
@@ -311,7 +319,7 @@ export function MarketingPage() {
311319
<p className="mkt-hero-sub">
312320
Postgres, Redis, MongoDB, vectors, queues, storage, webhooks, and deployments.{' '}
313321
<strong>Provisioned in &lt;2 seconds.</strong>{' '}
314-
No signup, no Docker, no waitlist.
322+
Self-serve from the first call to a paid plan — no signup, no Docker, no sales call.
315323
</p>
316324
<HeroPromptCard />
317325

0 commit comments

Comments
 (0)