diff --git a/apps/website/.env.example b/apps/website/.env.example index c6957548a..4449b83c5 100644 --- a/apps/website/.env.example +++ b/apps/website/.env.example @@ -12,3 +12,6 @@ RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx RESEND_AUDIENCE_ID=aud_xxxxxxxxxxxxxxxxxxxxxxxxxxxx RESEND_FROM="Angular Stream Resource " RESEND_NOTIFY_TO=hello@cacheplane.io + +# Loops.so (https://loops.so — free tier: 1,000 contacts) +LOOPS_API_KEY= diff --git a/apps/website/lib/loops.ts b/apps/website/lib/loops.ts new file mode 100644 index 000000000..f259d1dab --- /dev/null +++ b/apps/website/lib/loops.ts @@ -0,0 +1,60 @@ +const LOOPS_API_KEY = process.env.LOOPS_API_KEY || ''; +const LOOPS_BASE = 'https://app.loops.so/api/v1'; + +/** Create or update a contact in Loops. Fails silently. */ +export async function loopsUpsertContact(opts: { + email: string; + firstName?: string; + source?: string; + properties?: Record; +}) { + if (!LOOPS_API_KEY) { + console.info('[loops] skipped (no API key):', opts.email); + return; + } + try { + const body: Record = { + email: opts.email, + source: opts.source || 'website', + subscribed: true, + }; + if (opts.firstName) body.firstName = opts.firstName; + if (opts.properties) Object.assign(body, opts.properties); + + await fetch(`${LOOPS_BASE}/contacts/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${LOOPS_API_KEY}`, + }, + body: JSON.stringify(body), + }); + } catch (err) { + console.error('[loops] upsertContact failed:', err); + } +} + +/** Send an event to trigger a Loops workflow. Fails silently. */ +export async function loopsSendEvent(opts: { + email: string; + eventName: string; + properties?: Record; +}) { + if (!LOOPS_API_KEY) return; + try { + await fetch(`${LOOPS_BASE}/events/send`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${LOOPS_API_KEY}`, + }, + body: JSON.stringify({ + email: opts.email, + eventName: opts.eventName, + ...(opts.properties ? { eventProperties: opts.properties } : {}), + }), + }); + } catch (err) { + console.error('[loops] sendEvent failed:', err); + } +} diff --git a/apps/website/src/app/api/leads/route.ts b/apps/website/src/app/api/leads/route.ts index 50ef6bb70..769f704d1 100644 --- a/apps/website/src/app/api/leads/route.ts +++ b/apps/website/src/app/api/leads/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import fs from 'fs'; import path from 'path'; import { sendEmail, FROM, NOTIFY_TO, addToAudience } from '../../../../lib/resend'; +import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops'; import { leadNotificationHtml } from '../../../../emails/lead-notification'; const LEADS_FILE = path.join(process.cwd(), 'data', 'leads.ndjson'); @@ -40,6 +41,17 @@ export async function POST(req: NextRequest) { html: leadNotificationHtml({ name, email, company, message, ts }), }), addToAudience(email, name), + loopsUpsertContact({ + email, + firstName: name, + source: 'lead-form', + properties: { company }, + }), + loopsSendEvent({ + email, + eventName: 'lead_submitted', + properties: { company }, + }), ]); } catch (err) { console.error('[resend] lead notification failed:', err); diff --git a/apps/website/src/app/api/newsletter/route.ts b/apps/website/src/app/api/newsletter/route.ts index dda0e469d..bffda780e 100644 --- a/apps/website/src/app/api/newsletter/route.ts +++ b/apps/website/src/app/api/newsletter/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { sendEmail, FROM, addToAudience } from '../../../../lib/resend'; +import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops'; import { newsletterWelcomeHtml } from '../../../../emails/newsletter-welcome'; export async function POST(req: NextRequest) { @@ -26,6 +27,14 @@ export async function POST(req: NextRequest) { html: newsletterWelcomeHtml(), }), addToAudience(email), + loopsUpsertContact({ + email, + source: 'newsletter', + }), + loopsSendEvent({ + email, + eventName: 'newsletter_subscribed', + }), ]); } catch (err) { console.error('[resend] newsletter signup failed:', err); diff --git a/apps/website/src/app/api/whitepaper-signup/route.ts b/apps/website/src/app/api/whitepaper-signup/route.ts index 89b12f227..172166b77 100644 --- a/apps/website/src/app/api/whitepaper-signup/route.ts +++ b/apps/website/src/app/api/whitepaper-signup/route.ts @@ -2,6 +2,7 @@ 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 { whitepaperDownloadHtml } from '../../../../emails/whitepaper-download'; const SIGNUPS_FILE = path.join(process.cwd(), 'data', 'whitepaper-signups.ndjson'); @@ -39,6 +40,15 @@ export async function POST(req: NextRequest) { html: whitepaperDownloadHtml(name || undefined), }), addToAudience(email, name || undefined), + loopsUpsertContact({ + email, + firstName: name || undefined, + source: 'whitepaper', + }), + loopsSendEvent({ + email, + eventName: 'whitepaper_downloaded', + }), ]); } catch (err) { console.error('[resend] whitepaper email failed:', err);