pcbjam/site/src/pages/api/waitlist.ts

97 lines
3.3 KiB
TypeScript
Raw Normal View History

import type { APIRoute } from 'astro';
import { Resend } from 'resend';
// Opt this single route into on-demand (serverless) rendering. The rest of the
// site stays static; Vercel emits exactly one function for /api/waitlist.
export const prerender = false;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
// Runtime secrets (Vercel injects at runtime). Never prefix with PUBLIC_.
const RESEND_API_KEY = process.env.RESEND_API_KEY ?? import.meta.env.RESEND_API_KEY;
const RESEND_AUDIENCE_ID =
process.env.RESEND_AUDIENCE_ID ?? import.meta.env.RESEND_AUDIENCE_ID;
const FROM_EMAIL =
process.env.WAITLIST_FROM_EMAIL ??
import.meta.env.WAITLIST_FROM_EMAIL ??
'PCBJam <hello@pcbjam.com>';
function json(status: number, body: Record<string, unknown>) {
return new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
}
function redirect(status: 'ok' | 'error') {
// No-JS native submit: bounce back to the page with a status flag.
return new Response(null, {
status: 303,
headers: { Location: `/?waitlist=${status}#waitlist` },
});
}
export const POST: APIRoute = async ({ request }) => {
const ct = request.headers.get('content-type') ?? '';
const wantsJson = ct.includes('application/json');
let data: Record<string, unknown> = {};
try {
data = wantsJson
? await request.json()
: Object.fromEntries(await request.formData());
} catch {
return wantsJson ? json(400, { ok: false, error: 'bad_request' }) : redirect('error');
}
const email = String(data.email ?? '').trim().toLowerCase();
const honeypot = String(data.company_url ?? ''); // hidden field — must stay empty
const source = String(data.source ?? 'unknown');
// Bot caught by honeypot: silently "succeed" so we don't tip them off.
if (honeypot) return wantsJson ? json(200, { ok: true }) : redirect('ok');
if (!EMAIL_RE.test(email) || email.length > 254) {
return wantsJson ? json(400, { ok: false, error: 'invalid_email' }) : redirect('error');
}
// No key configured (e.g. local dev without secrets): accept + log, don't 500.
if (!RESEND_API_KEY) {
console.warn(`[waitlist] RESEND_API_KEY not set — skipping send. email=${email} source=${source}`);
return wantsJson ? json(200, { ok: true }) : redirect('ok');
}
try {
const resend = new Resend(RESEND_API_KEY);
if (RESEND_AUDIENCE_ID) {
await resend.contacts.create({
email,
unsubscribed: false,
audienceId: RESEND_AUDIENCE_ID,
});
}
await resend.emails.send({
from: FROM_EMAIL,
to: email,
subject: "You're on the PCBJam waitlist",
text: [
"You're on the list. 🎉",
'',
feat(site): 🫙 PCBJam landing revisions — branding, accuracy, positioning graphic Founder review pass on the landing page, plus commit the landing-page research/spec docs under features/lp. Branding & assets: - New PCBJam mark: convert the jam-jar image to favicon.svg (trimmed, rounded tile) and use it in the browser tab, header, and footer. - Footer "Built by": flower mark + "Emergence Engineering" in white Oswald (the blog's title font, self-hosted via @fontsource) so it's legible on dark. - Produce the positioning quadrant graphic (public/graphics) and drop the placeholder. Copy / accuracy: - Remove header GitHub link; point "Built by EE" + footer "About" to emergence-engineering.com; Built-by CTA -> mailto:contact@emergence-engineering.com. - Replace the wrong testimonial with Greg Detre's real quote from EE's site. - Drop blog icons from Multiplayer/Pillars/How-it-works; add "Coming soon" to live-cursors & pin-comment; drop the hard-coded Yjs mention. - Rework monetization to free+local / paid cloud-sync; remove "no metered AI", "no per-seat toll", and the Flux price claim; soften the AI section. - Reframe waitlist copy (incl. confirmation email) to newsletter + early access. Fixes: - Offset only #waitlist for the sticky header (scroll-margin-top); section anchors keep their original behavior. Docs: - Commit features/lp (copy spec, brand guide, competitive research) via force-add over the /features/ gitignore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:14:19 +02:00
"We'll keep you posted with product updates, and send your early-access invite",
'as seats open in waves. No spam — unsubscribe anytime.',
'',
'— The PCBJam team, built by Emergence Engineering',
].join('\n'),
});
return wantsJson ? json(200, { ok: true }) : redirect('ok');
} catch (err) {
console.error('[waitlist] send failed', err);
return wantsJson ? json(500, { ok: false, error: 'send_failed' }) : redirect('error');
}
};
// A bare GET (e.g. someone visiting the URL) shouldn't 500.
export const GET: APIRoute = () => json(405, { ok: false, error: 'method_not_allowed' });