MapleBrief: production Canadian news aggregator

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)
This commit is contained in:
2026-08-15 16:07:07 -04:00
commit 1010f27f44
76 changed files with 7177 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
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 });
}