Merge S7 (a1): newsletter — THE VYNDR REPORT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	BUILD-STATE.md
#	CLAUDE.md
This commit is contained in:
Kev
2026-07-11 14:32:18 -04:00
14 changed files with 1297 additions and 0 deletions
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
/**
* Newsletter subscribe proxy (Session S7, a1) — S25 rule: the browser hits
* the Next origin, never Express directly. Forwards to the backend, which
* forwards to the self-hosted Listmonk (double opt-in).
*
* Failure posture: any upstream problem degrades to the calm
* { ok: false, reason: 'not configured' } shape — the capture component
* renders "Signups open soon", never an error wall.
*/
export async function POST(req: NextRequest) {
let body: unknown = {};
try {
body = await req.json();
} catch {
return NextResponse.json({ ok: false, error: 'Enter a valid email.' }, { status: 400 });
}
try {
const upstream = await fetch(`${BACKEND_URL}/api/newsletter/subscribe`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(body),
cache: 'no-store',
});
const data = await upstream.json().catch(() => ({ ok: false, reason: 'not configured' }));
return NextResponse.json(data, { status: upstream.status === 400 ? 400 : 200 });
} catch {
return NextResponse.json({ ok: false, reason: 'not configured' }, { status: 200 });
}
}