Skip to Content
How it was builtDesign specs/resume page, PDF download and optional email capture

/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 the resume:pdf generation 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. Single h1 (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 from LandingSkills data; 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: avoid on 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-motion respected.
  • 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)

  1. Verify the Cloudflare Turnstile token. Reject without it.
  2. Rate-limit per IP (small fixed window).
  3. Validate: name optional, trimmed, max 100 chars, escaped in templates; email optional, strict format, max 254 chars. Body size capped.
  4. 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.
  5. Return 204 regardless of downstream send failures (log them). Nothing is persisted.

4. The PDF

  • scripts/resume-pdf.mjs (yarn resume:pdf) renders /resume print view with Playwright Chromium (already a dev dependency) to public/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)

ItemWhere
Resend API key, verified sending subdomain (e.g. send.antmejia.com) with SPF/DKIM/return-path records in CloudflareRESEND_API_KEY, RESUME_FROM, RESUME_NOTIFY_TO (Vercel env)
Turnstile site + secret keysNEXT_PUBLIC_TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY (Vercel env)
Content: summary, education, certifications, contact detailssrc/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: /resume 200, one h1, print emulation on Letter and A4 fits the intended pages; download starts with and without input.
  • type-check, lint, format:ci, yarn build in 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.