diff --git a/apps/website/lib/drip.ts b/apps/website/lib/drip.ts index 53a05102e..ac7c5991d 100644 --- a/apps/website/lib/drip.ts +++ b/apps/website/lib/drip.ts @@ -1,8 +1,20 @@ import { sendEmail, FROM } from './resend'; import { dripWhitepaperFollowupHtml } from '../emails/drip-whitepaper-followup'; +import { dripAngularFollowupHtml } from '../emails/drip-angular-followup'; +import { dripRenderFollowupHtml } from '../emails/drip-render-followup'; +import { dripChatFollowupHtml } from '../emails/drip-chat-followup'; + +export type PaperId = 'overview' | 'angular' | 'render' | 'chat'; const DRIP_DAYS = [2, 5, 10, 20]; +const DRIP_GENERATORS: Record { subject: string; html: string }> = { + overview: dripWhitepaperFollowupHtml, + angular: dripAngularFollowupHtml, + render: dripRenderFollowupHtml, + chat: dripChatFollowupHtml, +}; + function daysFromNow(days: number): string { const d = new Date(); d.setDate(d.getDate() + days); @@ -11,10 +23,10 @@ function daysFromNow(days: number): string { } /** Schedule the whitepaper drip sequence for a contact. Best-effort. */ -export async function scheduleWhitepaperDrip(email: string) { +export async function scheduleWhitepaperDrip(email: string, paper: PaperId = 'overview') { + const generator = DRIP_GENERATORS[paper] ?? DRIP_GENERATORS.overview; for (const day of DRIP_DAYS) { - const { subject, html } = dripWhitepaperFollowupHtml(day); - // Replace RECIPIENT placeholder with actual email for unsubscribe link + const { subject, html } = generator(day); const personalizedHtml = html.replace('email=RECIPIENT', `email=${encodeURIComponent(email)}`); try { await sendEmail({ @@ -25,7 +37,7 @@ export async function scheduleWhitepaperDrip(email: string) { scheduledAt: daysFromNow(day), }); } catch (err) { - console.error(`[drip] Failed to schedule day-${day} email for ${email}:`, err); + console.error(`[drip] Failed to schedule day-${day} ${paper} email for ${email}:`, err); } } } diff --git a/apps/website/public/assets/arch-diagram.svg b/apps/website/public/assets/arch-diagram.svg index adcbdf15d..5c54aea2e 100644 --- a/apps/website/public/assets/arch-diagram.svg +++ b/apps/website/public/assets/arch-diagram.svg @@ -25,18 +25,18 @@ - + streamResource() + font-size="15" font-weight="700" fill="#6C8EFF">agent() 12 BehaviorSubjects + font-size="11" fill="#4A527A">Signal-native state toSignal() at construction + font-size="11" fill="#4A527A">@cacheplane/angular @@ -79,14 +79,14 @@ font-family="'Courier New', monospace" font-size="11" fill="#4A527A" opacity="0.8">SSE / stream events - + BehaviorSubject.next() + font-size="11" fill="#4A527A" opacity="0.8">Signal updates - + FetchStreamTransport | MockStreamTransport + font-size="11" fill="#4A527A" opacity="0.7">FetchStreamTransport | MockAgentTransport diff --git a/apps/website/public/assets/hero.svg b/apps/website/public/assets/hero.svg index 12a2f2bc2..9f5cc199a 100644 --- a/apps/website/public/assets/hero.svg +++ b/apps/website/public/assets/hero.svg @@ -4,7 +4,7 @@ - + stream-resource + >cacheplane The Enterprise Agent Framework for LangChain and Angular + >The Angular Agent Framework for LangGraph diff --git a/apps/website/src/app/api/whitepaper-signup/route.ts b/apps/website/src/app/api/whitepaper-signup/route.ts index b880aa00c..15472d585 100644 --- a/apps/website/src/app/api/whitepaper-signup/route.ts +++ b/apps/website/src/app/api/whitepaper-signup/route.ts @@ -1,10 +1,32 @@ -// apps/website/src/app/api/whitepaper-signup/route.ts import { NextRequest, NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; +import { sendEmail, FROM, addToAudience } from '../../../../lib/resend'; +import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops'; +import { scheduleWhitepaperDrip, type PaperId } from '../../../../lib/drip'; +import { whitepaperDownloadHtml } from '../../../../emails/whitepaper-download'; +import { angularDownloadHtml } from '../../../../emails/angular-download'; +import { renderDownloadHtml } from '../../../../emails/render-download'; +import { chatDownloadHtml } from '../../../../emails/chat-download'; const SIGNUPS_FILE = path.join(process.cwd(), 'data', 'whitepaper-signups.ndjson'); +const VALID_PAPERS: PaperId[] = ['overview', 'angular', 'render', 'chat']; + +const DOWNLOAD_EMAILS: Record string> = { + overview: whitepaperDownloadHtml, + angular: angularDownloadHtml, + render: renderDownloadHtml, + chat: chatDownloadHtml, +}; + +const DOWNLOAD_SUBJECTS: Record = { + overview: 'Your Angular Agent Readiness Guide', + angular: 'Your Enterprise Guide to Agent Streaming', + render: 'Your Enterprise Guide to Generative UI', + chat: 'Your Enterprise Guide to Agent Chat Interfaces', +}; + export async function POST(req: NextRequest) { let body: { name?: string; email?: string; paper?: string }; try { @@ -13,18 +35,40 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } - const { name = '', email = '', paper = 'overview' } = body; + const name = (body.name || '').trim().slice(0, 200); + const email = (body.email || '').trim().slice(0, 320); + const paper = (VALID_PAPERS.includes(body.paper as PaperId) ? body.paper : 'overview') as PaperId; + if (!email || !email.includes('@')) { return NextResponse.json({ error: 'Valid email required' }, { status: 400 }); } - const entry = JSON.stringify({ name: name.trim(), email: email.trim(), paper: paper.trim(), ts: new Date().toISOString() }) + '\n'; + // Persist signup to NDJSON (always, even if email fails) + const entry = JSON.stringify({ name, email, paper, ts: new Date().toISOString() }) + '\n'; try { fs.mkdirSync(path.dirname(SIGNUPS_FILE), { recursive: true }); fs.appendFileSync(SIGNUPS_FILE, entry, 'utf8'); } catch (err) { console.error('Failed to write signup:', err); - return NextResponse.json({ error: 'Internal error' }, { status: 500 }); + } + + // Send download confirmation + schedule drip + sync contacts (best-effort) + try { + const downloadHtml = DOWNLOAD_EMAILS[paper](name || undefined); + await Promise.all([ + sendEmail({ + from: FROM, + to: email, + subject: DOWNLOAD_SUBJECTS[paper], + html: downloadHtml, + }), + scheduleWhitepaperDrip(email, paper), + addToAudience(email, name || undefined), + loopsUpsertContact({ email, firstName: name || undefined, source: `whitepaper-${paper}` }), + loopsSendEvent({ email, eventName: 'whitepaper_downloaded', properties: { paper } }), + ]); + } catch (err) { + console.error('[whitepaper-signup] email pipeline failed:', err); } return NextResponse.json({ ok: true });