/resume page, PDF download and optional email capture
Issue: #46 (sub-issue of #11). Status: design approved in conversation 2026-10-06; this spec is awaiting review.
Goal
/resume returns 200, shows the resume as an open, printable page, and offers a Download PDF action. Clicking it opens a dialog asking for name and email, both optional. The PDF downloads either way. If an email was given, the visitor is emailed the PDF, and the owner is notified of every entry.
Non-goals
- No gating: the page and the PDF are always reachable without entering anything.
- No storage of names or email addresses (no database, no spreadsheet).
- No invented content. Sections without owner-provided data are omitted.
Audience
Recruiters and hiring managers (skim scope and seniority, want a file to forward), then engineers (read the highlights). See PRODUCT.md.
Update: the owner supplied the PDF (
public/resume.pdf), so theresume:pdfgeneration script and CI drift check in section 4 were dropped. Education and the contact line were taken from that PDF.
Components
1. /resume page (src/app/resume/page.tsx)
- Uses
PageShell(back link to/) and the existing type, tokens and rules language. Singleh1(the name), semantic sections. - Content, in order: header (name, title, location “New York, NY”, Download PDF action); summary (only if provided by the owner); experience from
experienceTimeline; skills fromLandingSkillsdata; education and certifications (only if provided). - Contact details are printed only if the owner provides them.
- Print stylesheet: clean one-pager on US Letter and A4, no nav/footer, links readable,
break-inside: avoidon roles. - Added to
sitemap.ts. The footer link already points at/resume.
2. Download dialog
- Native
<dialog>or an accessible equivalent: focus trap, Esc closes, focus returns to the trigger,prefers-reduced-motionrespected. - Optional fields: name, email. Actions: Download and Skip and download. Both start the download immediately and close the dialog.
- If the email field is non-empty and valid, the dialog submits the request in the background. Failure never blocks or alters the download and shows no error.
- Invalid email: inline message, and the user can still skip.
3. POST /api/resume-request (route handler)
- Verify the Cloudflare Turnstile token. Reject without it.
- Rate-limit per IP (small fixed window).
- Validate: name optional, trimmed, max 100 chars, escaped in templates; email optional, strict format, max 254 chars. Body size capped.
- Send via Resend: an owner notification on every request (name/email if given, referrer, timestamp), and, only if an email was given, a fixed-template message to the visitor with the PDF attached. No visitor-controlled text other than the escaped name.
- Return 204 regardless of downstream send failures (log them). Nothing is persisted.
4. The PDF
scripts/resume-pdf.mjs(yarn resume:pdf) renders/resumeprint view with Playwright Chromium (already a dev dependency) topublic/resume.pdf.- CI regenerates it and fails if the extracted text differs from the committed file, so the PDF cannot drift from the page. Text comparison, not bytes, because Chromium output varies slightly across machines.
- The download button links to
/resume.pdf; the route handler attaches the same file.
Configuration (owner-provided)
| Item | Where |
|---|---|
Resend API key, verified sending subdomain (e.g. send.antmejia.com) with SPF/DKIM/return-path records in Cloudflare | RESEND_API_KEY, RESUME_FROM, RESUME_NOTIFY_TO (Vercel env) |
| Turnstile site + secret keys | NEXT_PUBLIC_TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY (Vercel env) |
| Content: summary, education, certifications, contact details | src/content/resume.ts (new, owner-supplied) |
Keys are never committed. Resend free tier (about 100/day) is sufficient; on exhaustion emails fail quietly and downloads still work.
Privacy
The dialog states what the details are used for (“so I know who is reading, and to send you a copy”). Names and emails are not stored by the site; they exist only in the emails sent through Resend.
Testing
- Storybook stories + browser tests for the dialog (open, skip, invalid email, keyboard, Esc, focus return).
- Route handler tests: missing/invalid token, rate limit, validation, Resend failure still returns 204.
- Playwright probe:
/resume200, oneh1, print emulation on Letter and A4 fits the intended pages; download starts with and without input. type-check,lint,format:ci,yarn buildin CI.
Risks and open decisions
- Content is not provided yet. Education, certifications, contact details and summary are omitted until supplied.
- Abuse: emailing arbitrary addresses is mitigated by Turnstile, rate limit, fixed template and escaped name, but remains a vector to watch.
- Deliverability: depends on the verified sending domain; confirm Resend’s recommended records against its current docs, including coexistence with Cloudflare Email Routing.
- Hobby limits: route handlers are fine; Vercel’s daily deployment quota applies to merges.