feat(site): 🛬 PCBJam landing page — waitlist, sections, Tailwind
Replace the placeholder homepage with the PCBJam waitlist conversion page
(13 sections from the copy spec), shared chrome, and a real email-capture
backend, keeping the site zero-runtime-JS / zero-web-font.
- Tailwind v4 via @tailwindcss/postcss (the Vite plugin breaks on Astro 6
rolldown-vite); Preflight skipped; brand tokens in :root + @theme inline.
- Waitlist: /api/waitlist serverless route (prerender=false) + Resend, with
dual JSON/303 responses; WaitlistForm progressive enhancement bound on
astro:page-load (survives ClientRouter swaps); honeypot + validation.
- New components (Icon, Placeholder, DemoVideo, SectionBand, Chip, EECredit,
StatBar, Pillar, Step, CheckItem, WaitlistForm) + 13 section components.
- Redesigned Header (sticky, mobile menu, CTA) + 4-col Footer (KiCad
attribution); BaseLayout gains full-bleed mode + OG/canonical meta.
- Assets scaffolded with marked placeholders; EE/generic/client SVGs copied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:59:13 +02:00
|
|
|
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 ??
|
2026-06-07 18:29:59 +02:00
|
|
|
'PCBJam <hello@pcbjam.com>';
|
feat(site): 🛬 PCBJam landing page — waitlist, sections, Tailwind
Replace the placeholder homepage with the PCBJam waitlist conversion page
(13 sections from the copy spec), shared chrome, and a real email-capture
backend, keeping the site zero-runtime-JS / zero-web-font.
- Tailwind v4 via @tailwindcss/postcss (the Vite plugin breaks on Astro 6
rolldown-vite); Preflight skipped; brand tokens in :root + @theme inline.
- Waitlist: /api/waitlist serverless route (prerender=false) + Resend, with
dual JSON/303 responses; WaitlistForm progressive enhancement bound on
astro:page-load (survives ClientRouter swaps); honeypot + validation.
- New components (Icon, Placeholder, DemoVideo, SectionBand, Chip, EECredit,
StatBar, Pillar, Step, CheckItem, WaitlistForm) + 13 section components.
- Redesigned Header (sticky, mobile menu, CTA) + 4-col Footer (KiCad
attribution); BaseLayout gains full-bleed mode + OG/canonical meta.
- Assets scaffolded with marked placeholders; EE/generic/client SVGs copied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:59:13 +02:00
|
|
|
|
|
|
|
|
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.',
|
feat(site): 🛬 PCBJam landing page — waitlist, sections, Tailwind
Replace the placeholder homepage with the PCBJam waitlist conversion page
(13 sections from the copy spec), shared chrome, and a real email-capture
backend, keeping the site zero-runtime-JS / zero-web-font.
- Tailwind v4 via @tailwindcss/postcss (the Vite plugin breaks on Astro 6
rolldown-vite); Preflight skipped; brand tokens in :root + @theme inline.
- Waitlist: /api/waitlist serverless route (prerender=false) + Resend, with
dual JSON/303 responses; WaitlistForm progressive enhancement bound on
astro:page-load (survives ClientRouter swaps); honeypot + validation.
- New components (Icon, Placeholder, DemoVideo, SectionBand, Chip, EECredit,
StatBar, Pillar, Step, CheckItem, WaitlistForm) + 13 section components.
- Redesigned Header (sticky, mobile menu, CTA) + 4-col Footer (KiCad
attribution); BaseLayout gains full-bleed mode + OG/canonical meta.
- Assets scaffolded with marked placeholders; EE/generic/client SVGs copied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 14:59:13 +02:00
|
|
|
'',
|
|
|
|
|
'— 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' });
|