We Generated Our Lead Magnet PDF from Code. Here's the Stack.
We needed a lead magnet for our AI services page. The goal was simple: someone enters their email, they get a PDF with five real AI automations, and we get their contact info into our CRM. No manual steps, no copy-pasting from form submissions, no checking inboxes.
We built it in one session. Here's exactly how it works.
The PDF Is Just HTML
Instead of designing the PDF in Canva or Figma and exporting a static file, we wrote the content in HTML and render it with Playwright Chromium at build time. The script lives at scripts/generate-lead-magnet-pdf.mjs. Run it, and it launches a headless browser, renders the HTML, and writes a Letter-format PDF to public/downloads/.
The typography is Inter for body copy, Playfair Display italic for the title, JetBrains Mono for labels and numbers. The design is intentional but entirely in CSS. When we update the content, we re-run the script and the PDF regenerates in seconds.
This matters for a reason that isn't obvious at first: a PDF stored in version control and generated from code is reproducible. You can update a URL, fix a typo, add a workflow, and redeploy. A Canva export is a one-way artifact. You lose the source of truth.
The Capture API Stays Thin
The landing page lives at /ai in our Next.js app. The lead form posts to /api/lead-capture, which does two things: validates that the email and name are real, then forwards the payload to an n8n webhook URL set via environment variable.
The API route doesn't touch a database. It doesn't send an email. It just validates and forwards. The payload it sends includes the email, name, source, path, timestamp, and a lead_magnet identifier so downstream nodes know which funnel this came from.
If the n8n webhook returns anything other than a 200, the API returns a 502 to the client. The form can show an error instead of a false success. Same pattern we applied when we fixed our contact form.
n8n Handles Everything Downstream
The webhook hits an n8n workflow. From there:
- The lead gets written to Airtable as a new row in our leads table.
- Gmail sends a welcome email with a direct link to the PDF.
- Slack gets a notification so we know when someone opted in.
None of that logic lives in the Next.js app. The API route has one job: get the data to n8n. n8n owns the routing, the CRM write, and the email send. If we want to add a follow-up sequence or route certain leads differently, we change the n8n workflow. The app code doesn't touch it.
Why This Approach
We sell AI automations to small businesses. We wanted the funnel we use for our own lead gen to be a working example of the same stack we build for clients: lightweight API boundary, n8n as the automation layer, downstream integrations in the workflow instead of baked into the app.
The whole system took a few hours to build. The PDF generation script is 230 lines. The capture API is 75 lines. The landing page is 135 lines. None of it is complicated. That's the point.
If you want to see the funnel live, it's at twenty1-media.com/ai.