From 24dfa0919c3cdb7c8eb04ea4111e7066e8456058 Mon Sep 17 00:00:00 2001 From: Matthew Meszaros Date: Wed, 27 May 2026 04:26:52 +0000 Subject: [PATCH] site(use-cases): full redesign of recruiters page, drops shared UseCasePage component --- site/src/pages/use-cases/recruiters.astro | 7 +- site/src/pages/use-cases/sales.astro | 652 +++++++++++++++++++++- 2 files changed, 630 insertions(+), 29 deletions(-) diff --git a/site/src/pages/use-cases/recruiters.astro b/site/src/pages/use-cases/recruiters.astro index 0a072a64..f3f2669e 100644 --- a/site/src/pages/use-cases/recruiters.astro +++ b/site/src/pages/use-cases/recruiters.astro @@ -112,8 +112,8 @@ const faq: [string, string][] = [ // Astro JSX caveat: hoist `<` and `<=` comparisons to the frontmatter where possible. const tipsForRecruiters = [ - 'No image-heavy job description — link to a hosted JD page instead.', - 'No HTML signature with logo, social links, or banner — plain text only.', + 'No image-heavy job description. Link to a hosted JD page instead.', + 'No HTML signature with logo, social links, or banner. Plain text only.', 'No tracking pixel on candidate outreach.', 'Use the recruiter\'s individual mailbox, not careers@ or talent@.', 'Keep the first message under 90 words.', @@ -349,7 +349,8 @@ We were both at {{ custom.connection }} "Recruiter" }} · {{ sender.agency }} diff --git a/site/src/pages/use-cases/sales.astro b/site/src/pages/use-cases/sales.astro index cea3381f..91e8bcdf 100644 --- a/site/src/pages/use-cases/sales.astro +++ b/site/src/pages/use-cases/sales.astro @@ -1,28 +1,628 @@ --- -import UseCasePage from '../../components/UseCasePage.astro'; +import Layout from '../../layouts/Layout.astro'; +import HeroAtmosphere from '../../components/HeroAtmosphere.astro'; +import Icon from '../../components/Icon.astro'; +import CTA from '../../components/CTA.astro'; + +// Platform defaults referenced throughout, all from CLAUDE.md / config: +// internal/config/constants.go · 100 / day cold cap, 600s gap +// internal/scheduler/warmup_scheduler.go · 40 / day warmup ceiling +// internal/repository/pg_worker.go · sender pool round-robin, health-aware + +// ===================== HERO MOCK · "Rep board" ===================== +// 8 BDRs/AEs, 2 mailboxes each, today's send count vs cap, reply spark, status. +// Numbers stay below the 100/day platform cold cap, with realistic spread. +type Status = 'healthy' | 'watch' | 'capped'; +type Mailbox = { addr: string; today: number; cap: number; status: Status }; +type Rep = { initials: string; tone: string; name: string; role: 'BDR' | 'AE'; territory: string; reply: number[]; mailboxes: [Mailbox, Mailbox] }; + +const reps: Rep[] = [ + { initials: 'BL', tone: 'bg-sky-100 text-sky-700', name: 'Ben Lee', role: 'BDR', territory: 'NA-East', reply: [2, 3, 2, 4, 5, 3, 4], + mailboxes: [ + { addr: 'ben@acme.com', today: 87, cap: 100, status: 'healthy' }, + { addr: 'ben@acmeinc.io', today: 64, cap: 100, status: 'healthy' }, + ] }, + { initials: 'SR', tone: 'bg-emerald-100 text-emerald-700', name: 'Sara Reyes', role: 'BDR', territory: 'NA-West', reply: [3, 4, 4, 5, 6, 5, 7], + mailboxes: [ + { addr: 'sara@acme.com', today: 93, cap: 100, status: 'healthy' }, + { addr: 'sara@acmeinc.io', today: 51, cap: 100, status: 'healthy' }, + ] }, + { initials: 'MO', tone: 'bg-amber-100 text-amber-700', name: 'Mark Okafor', role: 'BDR', territory: 'EMEA', reply: [1, 2, 2, 3, 2, 3, 3], + mailboxes: [ + { addr: 'mark@acme.com', today: 100, cap: 100, status: 'capped' }, + { addr: 'mark@acmeinc.io', today: 42, cap: 100, status: 'healthy' }, + ] }, + { initials: 'KN', tone: 'bg-violet-100 text-violet-700', name: 'Kai Nakamura', role: 'BDR', territory: 'APAC', reply: [4, 5, 6, 5, 7, 6, 8], + mailboxes: [ + { addr: 'kai@acme.com', today: 71, cap: 100, status: 'healthy' }, + { addr: 'kai@acmeinc.io', today: 56, cap: 100, status: 'healthy' }, + ] }, + { initials: 'JP', tone: 'bg-rose-100 text-rose-700', name: 'Jess Park', role: 'BDR', territory: 'NA-East', reply: [2, 2, 1, 2, 3, 2, 2], + mailboxes: [ + { addr: 'jess@acme.com', today: 38, cap: 100, status: 'watch' }, + { addr: 'jess@acmeinc.io', today: 47, cap: 100, status: 'healthy' }, + ] }, + { initials: 'AN', tone: 'bg-teal-100 text-teal-700', name: 'Amir Naseri', role: 'BDR', territory: 'EMEA', reply: [3, 3, 4, 4, 5, 4, 5], + mailboxes: [ + { addr: 'amir@acme.com', today: 81, cap: 100, status: 'healthy' }, + { addr: 'amir@acmeinc.io', today: 60, cap: 100, status: 'healthy' }, + ] }, + { initials: 'DV', tone: 'bg-indigo-100 text-indigo-700', name: 'Dana Vasquez', role: 'AE', territory: 'NA-West', reply: [5, 6, 5, 7, 6, 8, 7], + mailboxes: [ + { addr: 'dana@acme.com', today: 22, cap: 100, status: 'healthy' }, + { addr: 'dana@acmeinc.io', today: 14, cap: 100, status: 'healthy' }, + ] }, + { initials: 'TM', tone: 'bg-fuchsia-100 text-fuchsia-700', name: 'Tom Miller', role: 'AE', territory: 'NA-East', reply: [4, 5, 4, 6, 5, 7, 6], + mailboxes: [ + { addr: 'tom@acme.com', today: 18, cap: 100, status: 'healthy' }, + { addr: 'tom@acmeinc.io', today: 9, cap: 100, status: 'healthy' }, + ] }, +]; + +// Pre-compute aggregates for the rep-board summary strip. +let aggToday = 0; +let aggCap = 0; +let mboxCount = 0; +let healthyCount = 0; +for (const r of reps) { + for (const m of r.mailboxes) { + aggToday += m.today; + aggCap += m.cap; + mboxCount += 1; + if (m.status === 'healthy') healthyCount += 1; + } +} +const aggPct = Math.round((aggToday / aggCap) * 100); + +// Status chip styling (frontmatter-only comparisons). +const statusChip = (s: Status) => { + if (s === 'healthy') return { dot: 'bg-emerald-500', text: 'text-emerald-700', bg: 'bg-emerald-50', label: 'healthy' }; + if (s === 'watch') return { dot: 'bg-amber-500', text: 'text-amber-700', bg: 'bg-amber-50', label: 'watch' }; + return { dot: 'bg-rose-500', text: 'text-rose-700', bg: 'bg-rose-50', label: 'capped' }; +}; + +// Sparkline geometry — pre-compute polyline points to avoid JSX math noise. +const sparkPoints = (vals: number[]) => { + const w = 56, h = 16, max = Math.max(...vals, 1); + return vals.map((v, i) => { + const x = (i / (vals.length - 1)) * w; + const y = h - (v / max) * (h - 2) - 1; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }).join(' '); +}; + +// Cap utilisation tint for the per-mailbox bar. +const capTint = (today: number, cap: number) => { + const pct = (today / cap) * 100; + if (pct >= 95) return 'bg-rose-500'; + if (pct >= 80) return 'bg-amber-500'; + return 'bg-[#0284c7]'; +}; + +// ===================== ROUND-ROBIN FAN-OUT (8 mailboxes) ===================== +// Used by the diagram. Sorted by territory then by role so the visual lines up. +const fanMailboxes = reps.flatMap((r) => r.mailboxes.map((m) => ({ + addr: m.addr, role: r.role, today: m.today, cap: m.cap, initials: r.initials, tone: r.tone, status: m.status, +}))); + +// ===================== REPLY ROUTING POLICIES ===================== +const routingPolicies = [ + { name: 'Round-robin', body: 'Reply assigned to the next AE in the rotation, skipping those at quota.', code: 'route: round_robin(team: ae_us)', tone: 'bg-sky-50 text-sky-700' }, + { name: 'By territory', body: 'Reply assigned by the recipient domain or contact territory field on file.', code: 'route: by_territory(field: account.region)', tone: 'bg-violet-50 text-violet-700' }, + { name: 'By account owner', body: 'Reply assigned to the named owner in HubSpot or Salesforce.', code: 'route: crm.account_owner', tone: 'bg-emerald-50 text-emerald-700' }, + { name: 'Manual', body: 'Reply enters the team inbox triage queue. Manager assigns from there.', code: 'route: manual(queue: triage)', tone: 'bg-slate-100 text-slate-700' }, +]; + +const autoActions = [ + { label: 'Pause sequence', detail: 'Sequence halts for this contact across every step.' }, + { label: 'Slack notify', detail: 'Channel ping to the assignee with reply preview and 1-click claim.' }, + { label: 'Stage move', detail: 'CRM deal stage advances from Sequenced to Replied · Awaiting AE.' }, + { label: 'Calendar offer', detail: 'Optional reply-handler step inserts the assignee\'s booking link.' }, +]; + +// ===================== CRM SYNC SPEC ===================== +type SyncMode = 'two-way' | 'inbound' | 'outbound'; +type SyncRow = { object: string; hubspot: SyncMode; salesforce: SyncMode; pipedrive: SyncMode; note: string }; +const crmSync: SyncRow[] = [ + { object: 'Contact', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'Email, name, title, custom fields. Dedup on email.' }, + { object: 'Deal', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'Created on positive reply. Stage transitions sync both ways.' }, + { object: 'Activity', hubspot: 'outbound', salesforce: 'outbound', pipedrive: 'outbound', note: 'Every send, open, click, reply logged on the contact timeline.' }, + { object: 'Stage', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'CRM stage moves out of CRM pause the sequence here.' }, + { object: 'Owner', hubspot: 'two-way', salesforce: 'two-way', pipedrive: 'two-way', note: 'Reply routing reads CRM owner. Owner changes sync back.' }, +]; + +const modeBadge = (m: SyncMode) => { + if (m === 'two-way') return { txt: 'two-way', bg: 'bg-sky-50', fg: 'text-[#0369a1]', dot: 'bg-[#0284c7]' }; + if (m === 'inbound') return { txt: 'inbound', bg: 'bg-violet-50', fg: 'text-violet-700', dot: 'bg-violet-500' }; + return { txt: 'outbound', bg: 'bg-emerald-50', fg: 'text-emerald-700', dot: 'bg-emerald-500' }; +}; + +// ===================== FAQ ===================== +const faq: [string, string][] = [ + ['How do we distribute volume across reps without crushing one mailbox?', 'Add the rep mailboxes to a sender pool. The scheduler round-robins sequence sends across the pool with awareness of each mailbox\'s 100 / day cold cap, the 600s spacing rule, and the current health band. A mailbox that hits its cap is skipped for the rest of the day. The campaign does not stall.'], + ['Where do positive replies go?', 'Replies are classified, routed by the rule on the sequence (round-robin, territory, CRM account owner, or manual), and an assignment event fires. The assignment lands as a Slack notification, a CRM stage move, and an inbox claim for the named AE. The originating sequence is paused for that contact at the same moment.'], + ['Can a rep\'s mailbox issue affect the rest of the team?', 'Per-mailbox reputation is isolated. A mailbox that drifts into the watch band has its sends throttled and spacing widened. If it crosses into quarantine it leaves the sender pool entirely until it requalifies. The rest of the pool keeps sending at full pace.'], +]; + +// Frontmatter helper to avoid `<` and `<=` in JSX expressions. +const capRatio = (today: number, cap: number) => Math.min(100, Math.round((today / cap) * 100)); --- - + + +
+ + +
+
+ + Sales teams + + Built for teams of BDRs and AEs working a shared pipeline +
+ +

+ More reps. More mailboxes.
Volume follows. +

+

+ A sales team should not get outbound volume by squeezing each rep’s inbox. It should get volume by spreading sends across many healthy mailboxes and by routing every positive reply to the right AE in seconds. +

+ + +
+
+ + +
+
+
+ + +
+
+
Rep board · acme
+ + + Live · today + + {reps.length} reps · {mboxCount} mailboxes +
+
+ {aggToday} sent today + + {aggCap - aggToday} headroom + + {aggPct}% pool util. + + {healthyCount}/{mboxCount} healthy +
+
+ + + + + +
+ {reps.map((r) => { + const worst = r.mailboxes.reduce((acc: Status, m) => { + if (m.status === 'capped' || acc === 'capped') return 'capped'; + if (m.status === 'watch' || acc === 'watch') return 'watch'; + return 'healthy'; + }, 'healthy' as Status); + const chip = statusChip(worst); + return ( +
+ +
+ {r.initials} +
+
{r.name}
+
{r.role} · {r.territory}
+
+
+ + +
+ {r.mailboxes.map((m) => { + const pct = capRatio(m.today, m.cap); + return ( +
+
{m.addr}
+
+
+
+
{m.today}/{m.cap}
+
+ ); + })} +
+ + +
+ + + + {r.reply[r.reply.length - 1]}% +
+ + +
+ + + {chip.label} + +
+
+ ); + })} +
+ + +
+ Per-mailbox cap: 100 / day · spacing 600s · enforced server-side + +
+
+
+
+ + +
+
+
+
+
Round-robin pool
+

+ One campaign.
Eight rep mailboxes. +

+

+ A sequence is bound to a sender pool, not a mailbox. The scheduler distributes recipients across every mailbox in the pool, respects the per-mailbox 100 / day cap, holds to the 600s spacing rule, and skips any mailbox that has dropped out of the healthy band. +

+
    +
  • Cap-aware: a 100 / 100 mailbox is skipped for the day.
  • +
  • Health-aware: a mailbox in the watch band gets fewer sends.
  • +
  • Spacing-aware: sends from one mailbox sit ~10 minutes apart.
  • +
  • Pool grows when you add reps, not when you raise a cap.
  • +
+
+ + +
+
+
sequence: acme-q2-outbound
+
pool size: {fanMailboxes.length}
+
+
+ + + + + + + + + + + + SEQUENCE + acme-q2-outbound + 412 recipients + + + + + + RR + cap+health + + + + + {fanMailboxes.map((m, i) => { + const yTop = 18 + i * 53; + const dur = 4 + (i % 4) * 0.4; + const begin = (i * 0.45).toFixed(2); + const capColor = m.status === 'capped' ? '#f43f5e' : m.status === 'watch' ? '#f59e0b' : '#0284c7'; + return ( + + + + {m.initials} + {m.addr} + {m.role} · {m.today}/{m.cap} + + + + {m.status !== 'capped' && ( + + + + )} + + ); + })} + +
+
+ Round-robin · cap-aware · health-aware + Capped lanes paused, sends rebalance to healthy mailboxes +
+
+
+
+
+ + +
+
+
+
Reply routing
+

+ A positive reply lands in the right AE’s lap in seconds. +

+

+ Every reply runs through classification, then the routing rule on the sequence picks the assignee. The platform fires a fixed set of actions on the same event so nothing relies on a human noticing first. +

+
+ + +
+
+ +
+
+ 01 + Reply lands +
+
Inbound to rep mailbox
+
+
from: dana.li@northwind.io
+
to: ben@acme.com
+
“Yes, this is timely. Could we put 20 minutes on the calendar next week?”
+
+
in-reply-to: seq:acme-q2-outbound · step 03
+
+ + + + + +
+
+ 02 + Classified +
+
Intent + sentiment
+
    +
  • Intentmeeting_request
  • +
  • Sentimentpositive
  • +
  • OOOfalse
  • +
  • Unsubscribefalse
  • +
+
routing: by_territory(field: account.region)
+
+ + + + + +
+
+ 03 + Routed +
+
Assigned to AE
+
+ DV +
+
Dana Vasquez
+
AE · NA-West
+
+
+
+
+ sequence + paused +
+
+ slack + #ae-na-west pinged +
+
+ crm stage + replied → awaiting ae +
+
+
+
+
+ + +
+ +
+
Routing policies
+
+ {routingPolicies.map((p) => ( +
+ {p.name} +
+

{p.body}

+
{p.code}
+
+
+ ))} +
+
+ + +
+
Auto-actions fired on assignment
+
    + {autoActions.map((a, i) => ( +
  • + {String(i + 1).padStart(2, '0')} +
    +
    {a.label}
    +

    {a.detail}

    +
    +
  • + ))} +
+
+ All four fire on the same event. No human-in-the-loop delay between them. +
+
+
+
+
+ + +
+
+
+
CRM sync
+

+ CRM of record stays the source of truth. +

+

+ The sales team’s CRM does not get a separate copy of pipeline. Warmbly syncs contacts, deals, activity, stage, and ownership both ways with HubSpot, Salesforce, and Pipedrive. A stage moved in the CRM is honoured here on the next pass. +

+
+ +
+ +
+
Object
+
HubSpot
+
Salesforce
+
Pipedrive
+
Notes
+
+
+ {crmSync.map((row) => { + const h = modeBadge(row.hubspot); + const s = modeBadge(row.salesforce); + const p = modeBadge(row.pipedrive); + return ( +
+
{row.object}
+
+ + {h.txt} + +
+
+ + {s.txt} + +
+
+ + {p.txt} + +
+
{row.note}
+
+ ); + })} +
+
+ Dedup on email. Last-writer-wins on stage and owner. + Webhook events for every sync transition. +
+
+
+
+ + +
+
+
+
Sales team FAQ
+

+ Three questions sales ops asks first. +

+

+ More detail in the campaigns and inbox pages. +

+
+
+ {faq.map(([q, a]) => ( +
+ +
+
+

{a}

+
+
+
+ ))} +
+ + +
+
+ + +