Sync newsletter opt-ins to Brevo
Both standalone signup forms (Newsletter.tsx on Home/newsletter page, NewsletterModal.tsx from the Navbar CTA) were previously non-functional — static markup with no onSubmit/state at all, nothing was ever captured. They're now real client forms posting to the new /api/newsletter/subscribe route, which upserts the contact into Brevo's Contacts API (list id from BREVO_LIST_ID). Checkout's existing newsletterOptIn checkbox gets the same sync, fire-and-forget alongside the order-confirmation email — a failed marketing sync must never fail checkout. lib/brevo.ts is the only thing that talks to Brevo; this app still never sends marketing mail itself. Whatever automation Brevo has configured on the list (Welcome Flow etc.) runs entirely on their side — Brevo's Automation workflows aren't manageable via their public API at all, so that part can't be wired up from here. Needs BREVO_API_KEY and BREVO_LIST_ID set in the frontend's Coolify environment — not yet added there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
// Server-only — syncs newsletter opt-ins to Brevo's Contacts API. Brevo
|
||||
// owns everything downstream of that (list membership, unsubscribe links,
|
||||
// and whatever Welcome Flow automation is configured on the list in
|
||||
// Brevo's own UI — that automation isn't manageable via their public API
|
||||
// at all, only contacts/lists are). This app never sends marketing mail
|
||||
// itself; it only ever hands Brevo the contact + consent.
|
||||
const BREVO_API_URL = "https://api.brevo.com/v3/contacts";
|
||||
|
||||
export type BrevoSyncResult = { ok: true } | { ok: false; reason: string };
|
||||
|
||||
// `source` becomes a Brevo contact attribute so campaigns/segments can
|
||||
// tell a checkout opt-in apart from the standalone signup forms without
|
||||
// needing separate lists.
|
||||
export async function upsertNewsletterContact(
|
||||
email: string,
|
||||
source: "checkout" | "newsletter-page" | "newsletter-modal",
|
||||
): Promise<BrevoSyncResult> {
|
||||
const apiKey = process.env.BREVO_API_KEY;
|
||||
const listId = process.env.BREVO_LIST_ID;
|
||||
if (!apiKey || !listId) {
|
||||
return { ok: false, reason: "BREVO_API_KEY/BREVO_LIST_ID nicht konfiguriert." };
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(BREVO_API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"api-key": apiKey,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
listIds: [Number(listId)],
|
||||
updateEnabled: true,
|
||||
attributes: { OPT_IN_SOURCE: source },
|
||||
}),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
// 204 for both a fresh contact and an existing one (updateEnabled
|
||||
// above merges the list membership onto the existing contact instead
|
||||
// of erroring).
|
||||
if (res.ok || res.status === 204) return { ok: true };
|
||||
const body = await res.json().catch(() => null);
|
||||
return { ok: false, reason: body?.message ?? `Brevo antwortete mit ${res.status}` };
|
||||
} catch (err) {
|
||||
return { ok: false, reason: err instanceof Error ? err.message : "Brevo ist gerade nicht erreichbar." };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user