Archived
Next.js 14 + TypeScript + Tailwind + Prisma/SQLite. - LLM synthesis abstraction (OpenAI/Anthropic/Ollama) + admin settings UI - RSS ingestion pipeline (parser + cheerio content) + node-cron worker - AdSense AdUnit placements (header/in-feed/in-article/sidebar) - Sources analysis attribution, nofollow links, canonical/OG, sitemap/robots - Admin auth (ADMIN_API_KEY, timingSafeEqual, fail-closed 401) - Multi-cell lady-ga-ga marker, sitemap, robots, legal pages - Comprehensive README (setup, LLM config, AdSense, migrations, deploy)
45 lines
1.3 KiB
TypeScript
45 lines
1.3 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
export const runtime = 'nodejs';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
interface ContactPayload {
|
|
email?: string;
|
|
topic?: string;
|
|
url?: string;
|
|
message?: string;
|
|
}
|
|
|
|
/**
|
|
* Placeholder contact sink.
|
|
*
|
|
* Product wiring options (in order of least effort):
|
|
* 1. Email: swap the `console.log` for `nodemailer` or an external
|
|
* transactional API (Resend, Postmark, SendGrid).
|
|
* 2. Form service: point the fetch at Formspree/Getform instead.
|
|
*
|
|
* The admin UI replies to itself and there is currently no secret channel;
|
|
* in production add a rate limit (e.g., 5/hour/IP) and CAPTCHA.
|
|
*/
|
|
export async function POST(req: NextRequest) {
|
|
let body: ContactPayload;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ ok: false, error: 'invalid JSON' }, { status: 400 });
|
|
}
|
|
|
|
if (!body.email || !body.message) {
|
|
return NextResponse.json(
|
|
{ ok: false, error: 'email and message are required' },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Sensitive-ish: keep payload out of stdout logs in DEBUG mode.
|
|
console.log(`[contact] topic=${body.topic ?? 'other'} from=${body.email} url=${body.url ?? '-'}`);
|
|
console.log(`[contact] message: ${String(body.message).slice(0, 500)}`);
|
|
|
|
return NextResponse.json({ ok: true });
|
|
}
|