Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions apps/website/lib/drip.ts
Original file line number Diff line number Diff line change
@@ -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<PaperId, (day: number) => { 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);
Expand All @@ -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({
Expand All @@ -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);
}
}
}
16 changes: 8 additions & 8 deletions apps/website/public/assets/arch-diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions apps/website/public/assets/hero.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 48 additions & 4 deletions apps/website/src/app/api/whitepaper-signup/route.ts
Original file line number Diff line number Diff line change
@@ -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<PaperId, (name?: string) => string> = {
overview: whitepaperDownloadHtml,
angular: angularDownloadHtml,
render: renderDownloadHtml,
chat: chatDownloadHtml,
};

const DOWNLOAD_SUBJECTS: Record<PaperId, string> = {
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 {
Expand All @@ -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 });
Expand Down
Loading