From 0c4b0eda88e08cd825dcea70eed188a397ca9590 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 9 Oct 2026 17:19:14 -0700 Subject: [PATCH] feat(lifecycle): link founder booking through threadplane.ai/call Resend flags links whose host does not match the sending domain. Campaign mail now links to https://threadplane.ai/call, a temporary website redirect to the Google Calendar booking page, and the raw calendar.app.google URL is no longer an approved campaign link. Co-Authored-By: Claude Opus 5.5 --- apps/lifecycle/src/campaign/templates.spec.ts | 1 + apps/lifecycle/src/campaign/templates.ts | 11 ++++++----- apps/website/next.config.spec.ts | 18 ++++++++++++++++++ apps/website/next.config.ts | 8 ++++++++ ...1-threadplane-growth-lifecycle-v1-design.md | 2 +- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/apps/lifecycle/src/campaign/templates.spec.ts b/apps/lifecycle/src/campaign/templates.spec.ts index 7e17a8508..cbdc960e4 100644 --- a/apps/lifecycle/src/campaign/templates.spec.ts +++ b/apps/lifecycle/src/campaign/templates.spec.ts @@ -79,6 +79,7 @@ describe('renderCampaignTemplate', () => { }) ).toEqual([]); for (const link of [ + 'https://calendar.app.google/nK961tWHZd21izKR6', 'https://calendar.app.google/someone-else', 'https://calendly.com/threadplane/demo', 'https://cal.com/threadplane/demo', diff --git a/apps/lifecycle/src/campaign/templates.ts b/apps/lifecycle/src/campaign/templates.ts index 2b205d4a7..c52bd319a 100644 --- a/apps/lifecycle/src/campaign/templates.ts +++ b/apps/lifecycle/src/campaign/templates.ts @@ -14,12 +14,13 @@ export type CampaignEvidenceAngle = const CampaignStepSchema = z.enum(['immediate', 'day-3', 'day-8']); /** - * Brian's Google Calendar appointment-schedule booking page. This is the only - * scheduling link recipient copy may carry; every other calendar host or path - * is still rejected by the draft checks below. + * First-party redirect to Brian's Google Calendar appointment-schedule booking + * page (configured in the website's `next.config.ts`). Recipient copy links + * here instead of the calendar host so every URL matches the sending domain. + * This is the only scheduling link recipient copy may carry; every other + * calendar host or path is still rejected by the draft checks below. */ -export const FOUNDER_BOOKING_URL = - 'https://calendar.app.google/nK961tWHZd21izKR6'; +export const FOUNDER_BOOKING_URL = 'https://threadplane.ai/call'; /** * Every link recipient copy may carry, across campaign steps and fulfillment diff --git a/apps/website/next.config.spec.ts b/apps/website/next.config.spec.ts index e743db509..834ed19d0 100644 --- a/apps/website/next.config.spec.ts +++ b/apps/website/next.config.spec.ts @@ -96,3 +96,21 @@ describe('website next.config redirects for the removed chat configuration API', } }); }); + +/** + * Lifecycle mail links to `/call` rather than the Google Calendar host so every + * URL in the message matches the sending domain. The redirect must stay + * temporary so the booking page can move without stale browser caches. + */ +describe('website next.config founder booking redirect', () => { + it('temporarily redirects /call to the Google Calendar booking page', async () => { + const redirects = await config.redirects!(); + const rule = redirects.find((r: { source: string }) => r.source === '/call'); + + expect(rule).toBeTruthy(); + expect(rule.destination).toBe( + 'https://calendar.app.google/nK961tWHZd21izKR6' + ); + expect(rule.permanent).toBe(false); + }); +}); diff --git a/apps/website/next.config.ts b/apps/website/next.config.ts index 1f23af451..61928c2c3 100644 --- a/apps/website/next.config.ts +++ b/apps/website/next.config.ts @@ -85,6 +85,14 @@ export const nextConfig: WithNxOptions = { destination: '/api/markdown/chat/getting-started/installation', permanent: true, }, + // Founder booking link for lifecycle mail. Recipient copy carries this + // first-party URL so every link matches the sending domain; the temporary + // redirect lets the calendar page change without touching sent mail. + { + source: '/call', + destination: 'https://calendar.app.google/nK961tWHZd21izKR6', + permanent: false, + }, ], rewrites: async () => [ { diff --git a/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md b/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md index 0d1f70c28..3c98ac9a9 100644 --- a/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md +++ b/docs/superpowers/specs/2026-08-31-threadplane-growth-lifecycle-v1-design.md @@ -679,7 +679,7 @@ Copy constraints: - At most one question and at most one useful link. - No banner, button, HTML layout, tracking pixel, open tracking, or click rewriting. - Never say “I saw you…” based on telemetry. -- The only scheduling link allowed is Brian’s Google Calendar appointment-schedule booking page (`FOUNDER_BOOKING_URL` in the campaign templates); every other calendar host or path is rejected. +- The only scheduling link allowed is Brian’s Google Calendar appointment-schedule booking page, linked through the first-party `https://threadplane.ai/call` redirect so URLs match the sending domain (`FOUNDER_BOOKING_URL` in the campaign templates); every other calendar host or path is rejected. - From and Reply-To are Brian at Threadplane . - BCC Brian on every recipient-facing email that may begin a conversation. The email carries X-Threadplane-Job-ID so the Google poller can register the BCC seed’s actual RFC Message-ID; seed copies are never treated as recipient replies.