Ebook Landing Page with Email Delivery
Build a landing page that automatically emails a PDF ebook to subscribers.
Introduction
30 Second Summary
Every day, creators and businesses share free ebooks, guides, and templates to grow their audience. Behind the scenes, someone has to wire up the system that collects an email address and delivers the file instantly.
In this project, you will build a landing page that collects email addresses and automatically sends a PDF ebook to each visitor who submits the form. The entire system deploys for free using Vercel and Resend.
What You'll Build
A professional landing page backed by a serverless function that calls the Resend API to deliver a PDF attachment to any visitor who enters their email.
By the end of this project, you'll have:
- A responsive landing page with an email capture form built in HTML, CSS, and JavaScript.
- A Node.js serverless function that receives form submissions and sends emails with a PDF attachment.
- A live deployed URL on Vercel that anyone can visit to request your ebook.
- Secret Mission: a thank you page with a live download counter that tracks how many ebooks have been sent.
Are there any prerequisites?
None. This project includes full setup instructions for every tool you need, including Node.js, VS Code, the Vercel CLI, and your Resend account.
Set Up Your Tools
Before you can build your ebook delivery system, you need three tools in place.
You need Node.js to run JavaScript on your machine, a Resend account to send emails through their API, and the Vercel CLI to deploy your project to the internet.
In this step, get ready to:
- Install and verify Node.js.
- Create a Resend account and generate an API key.
- Create a Vercel account and install the Vercel CLI.
Install and verify Node.js
Node.js lets you run JavaScript outside the browser. The Vercel CLI and your serverless function both depend on it.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
- Check if Node.js is installed by running this command:
node --version
✔️ I see version 18 or higher
You're all set. Move on to the next substep.
ⓧ I see a version lower than 18
The Vercel CLI requires Node.js 18 or higher. You need to upgrade your installation.
- Visit the official Node.js download page at nodejs.org.
- Download the LTS installer for your operating system.
- Run the installer and accept the default settings.
- Close and reopen your terminal, then run node --version again to confirm you see v18 or higher.
ⓧ Command not found
Node.js is not installed on your machine. Let's install it now.
- Visit the official Node.js download page at nodejs.org.
- Download the LTS installer for your operating system.
- Run the installer and accept the default settings.
- Close and reopen your terminal, then run node --version again to confirm you see a version number like v18.x.x or higher.
Create a Resend account and generate an API key
Resend is the email delivery service that will send your ebook to readers. You need an account and an API key so your code can authenticate with their servers.
- Open your browser and go to resend.com.
- Click Get Started and create a free account. No credit card is required.
- Once logged in, click API Keys in the left sidebar.
- Click Create API Key.
- Give your key a name like ebook-project.
- Select Full access as the permission.
- Click Create.
Copy your API key now
Resend only shows the API key value once. Copy it immediately and paste it into a safe place like a text file or notes app. You will not be able to see it again.
What is an API key?
An API key is a secret token that identifies your account when your code makes requests to a service. It works like a password that proves your serverless function is authorized to send emails through Resend.
Create a Vercel account and install the Vercel CLI
Vercel is the hosting platform that will serve your landing page and run your serverless function. The CLI tool lets you deploy and manage your project from the terminal.
- Open your browser and go to vercel.com.
- Click Sign Up and create a free Hobby account.
What is the Vercel Hobby plan?
Vercel's Hobby plan is completely free and includes 1,000,000 serverless function invocations per month. That's more than enough for this project.
- Back in your terminal, install the Vercel CLI globally by running this command:
npm i -g vercel
What does this command do?
- `npm i -g` installs a package globally on your machine so you can run it from any folder.
- `vercel` is the CLI package that gives you access to the vercel command.
- Log in to Vercel from the terminal by running:
vercel login
This opens a browser window where you can approve the login. Follow the prompts to authenticate with your Vercel account.
Permission error when installing globally?
On macOS or Linux, you may need to prefix the install command with sudo. On Windows, try running your terminal as Administrator.
help me fix a permission error when installing npm packages globally
- Confirm the CLI is installed by running:
vercel --version
You should see a version number printed in the terminal.
Your tools are ready. Next up, you'll create the project structure and write the code for your landing page.
Create Your Project Structure
Your tools are installed and your accounts are ready. Now you need a project folder with the right structure for Vercel to understand.
Vercel expects a specific layout: static files in a public directory and serverless functions in an api directory. In this step, you'll scaffold the project and install the Resend SDK that powers email delivery.
In this step, get ready to:
- Initialize a Node.js project and install the Resend SDK.
- Create the api and public directories.
- Add a sample PDF file that will serve as your ebook attachment.
Initialize a Node.js project and install the Resend SDK
Every Node.js project starts with a package.json file. This file declares your project's name, version, and the packages it depends on.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir ebook-landing-page
cd ebook-landing-page
- Confirm the folder was created by running:
ls
You should see an empty output since the folder is brand new.
- Initialize the project by running this command:
npm init -y
What does this command do?
The npm init -y command creates a package.json file with default values. The -y flag skips the interactive prompts and fills in sensible defaults automatically.
The default package.json has extra fields your project does not need. You'll replace it with a cleaner version that includes two important settings: "type": "module" (to enable modern import syntax) and the Resend dependency.
- Open VS Code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the ebook-landing-page folder, and click Open.
- In the Explorer sidebar, click on package.json to open it.
- Select all the text in the file (Cmd+A on macOS or Ctrl+A on Windows) and delete it.
- Replace the contents with this code:
{
"name": "ebook-landing-page",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"resend": "^6.12.3"
}
}
What do these fields mean?
- "private": true prevents you from accidentally publishing this project as a public npm package.
- "type": "module" lets you use modern import statements instead of the older require() syntax.
- "resend": "^6.12.3" declares the Resend SDK as a dependency that npm will install for you.
- Save package.json (Cmd+S on macOS or Ctrl+S on Windows).
- Install the Resend SDK by running this command in your terminal:
npm install
You should see output mentioning added packages. A new node_modules folder and a package-lock.json file will appear in the Explorer sidebar.
Seeing an error from npm install?
- Make sure your terminal is inside the ebook-landing-page folder. Run pwd to check your current directory.
- Check that you saved package.json before running npm install. Unsaved changes mean npm cannot find the dependency list.
Still stuck? Help me troubleshoot npm install failing in my ebook-landing-page project
✔️ Awesome, I've got everything!
Double check you've saved package.json and that node_modules appeared in your Explorer sidebar.
ⓧ I'd like to double check the full code
Here is what your package.json should look like:
{
"name": "ebook-landing-page",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"resend": "^6.12.3"
}
}
Create the api and public directories
Vercel uses folder names as a convention. Files inside api/ automatically become serverless function endpoints. Files inside public/ are served as static assets (your landing page).
- Create both directories by running this command:
mkdir api public
- Confirm the directories exist by running:
ls
You should see api, node_modules, package-lock.json, package.json, and public in the output.
Why this folder structure?
Vercel automatically routes requests to files in the api/ folder as serverless function endpoints. A file at api/send-ebook.js becomes accessible at /api/send-ebook on your deployed site. No server configuration needed.
Add a sample PDF as your ebook
Your serverless function will attach a PDF to the email it sends. You need a PDF file in your project to serve as the ebook.
- Find any small PDF file on your computer. A one-page document works perfectly for testing.
- Copy or move the PDF into the public folder inside your ebook-landing-page project.
- Rename the file to ebook.pdf.
No PDF handy?
Open any text editor (TextEdit, Notepad, or Google Docs), type a few lines of placeholder text, and export or print it as a PDF. Save it as ebook.pdf in your public folder.
- Confirm the file is in the right place by running:
ls public
You should see ebook.pdf in the output.
Don't see ebook.pdf listed?
- Make sure you placed the file inside the public folder, not in the project root.
- Check that the file is named exactly ebook.pdf (lowercase, no spaces).
Need a hand? Help me get my ebook.pdf into the right folder
Your project skeleton is ready. Next up, you'll build the landing page that visitors see when they arrive at your site.
Build the Landing Page
Your project structure is ready with a public directory, an api directory, and your ebook PDF waiting to be delivered.
Now you need an actual page that visitors can see and interact with. In this step, you'll build a landing page with an email capture form, style it with CSS, and wire up JavaScript to send submissions to your API endpoint.
In this step, get ready to:
- Create the HTML structure with a heading, description, and email form.
- Style the page with CSS for a polished, responsive design.
- Add JavaScript to handle form submissions and communicate with the API.
Create the HTML file with the email form
The landing page needs a clear headline, a brief description of what visitors will receive, and a form where they can type their email address.
- In VS Code, create a new file called index.html inside your public folder.
- Paste the following HTML into the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Get Your Free Ebook</title>
<!-- Link to our stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<div class="card">
<div class="book-icon">📚</div>
<h1>Master the Basics</h1>
<p class="subtitle">A free guide to getting started with web development</p>
<p class="description">Enter your email below and we'll send the ebook straight to your inbox as a PDF attachment.</p>
<!-- Email capture form -->
<form id="ebook-form">
<input
type="email"
id="email"
name="email"
placeholder="your@email.com"
required
>
<button type="submit" id="submit-btn">Send Me the Ebook</button>
</form>
<!-- Status message shown after form submission -->
<p id="message" class="message"></p>
</div>
</div>
<!-- Form handling script -->
<script src="script.js"></script>
</body>
</html>
What does this HTML do?
The page is built around a single .card div that holds everything the visitor sees. Inside the card, there's a headline, a subtitle, and a description explaining what they'll get.
The <form> with id ebook-form captures the email input. The empty <p id="message"> element is where success or error messages will appear after submission.
- Save index.html.
- Open the file in your browser by right-clicking index.html in your file explorer and selecting Open With followed by your browser (Chrome, Safari, etc.).
- You should see unstyled text with a heading that says "Master the Basics", a plain text input, and a button.
See a blank page?
Make sure the file is saved inside the public folder and has the .html extension. If you see raw code instead of a page, your browser might be opening it as a text file.
help me figure out why my HTML file isn't rendering in the browser
Style the page with CSS
The HTML looks plain right now. You'll add CSS to give the page a gradient background, center the card on screen, and style the form inputs to look professional.
- Create a new file called style.css inside the public folder.
- Add the page layout styles by pasting the following CSS:
/* Reset default browser spacing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Full-page gradient background with centered content */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* Constrain the card width */
.container {
width: 100%;
max-width: 480px;
}
/* White card with shadow for depth */
.card {
background: white;
border-radius: 16px;
padding: 48px 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
text-align: center;
}
What does this CSS do?
The * reset removes default browser margins so spacing is consistent. The body uses flexbox to center the card both horizontally and vertically on a purple gradient background.
The .card class creates a white box with rounded corners and a subtle shadow, giving it the appearance of floating above the gradient.
- Save style.css and refresh your browser.
- You should see a purple-to-violet gradient background with a white card centered on the page.
Still seeing unstyled text?
Check that style.css is in the same public folder as index.html. The <link> tag uses a relative path, so both files must be side by side.
help me figure out why my CSS isn't loading
- Now add the content and form styles below the existing CSS in the same file:
/* Book emoji size */
.book-icon {
font-size: 48px;
margin-bottom: 16px;
}
/* Page heading */
h1 {
font-size: 28px;
color: #1a1a2e;
margin-bottom: 8px;
}
/* Colored subtitle text */
.subtitle {
color: #667eea;
font-weight: 600;
margin-bottom: 16px;
}
/* Description paragraph */
.description {
color: #555;
line-height: 1.6;
margin-bottom: 32px;
}
/* Stack form elements vertically */
form {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Email input field styling */
input[type="email"] {
padding: 14px 18px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
outline: none;
}
/* Highlight border on focus */
input[type="email"]:focus {
border-color: #667eea;
}
/* Submit button */
button {
padding: 14px 18px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
/* Darken button on hover */
button:hover {
background: #5a6fd6;
}
/* Grey out button while sending */
button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Status message area */
.message {
margin-top: 16px;
font-size: 14px;
min-height: 20px;
}
/* Green text for success */
.message.success {
color: #22c55e;
}
/* Red text for errors */
.message.error {
color: #ef4444;
}
What do these styles do?
The typography styles make the heading, subtitle, and description visually distinct. The form uses flexbox with a gap to space out the input and button vertically.
The button:disabled style greys out the button while the form is submitting. The .message.success and .message.error classes change the text color to green or red depending on the API response.
- Save style.css and refresh your browser.
- You should see a polished card with a book emoji, styled heading in dark text, a purple subtitle, a rounded email input, and a purple "Send Me the Ebook" button.
Button or input look wrong?
Make sure you pasted the second CSS chunk below the first one in the same file. If styles are split across two files, only one will load. Both chunks should be in a single style.css file.
help me fix my CSS layout
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete style.css file for comparison:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 480px;
}
.card {
background: white;
border-radius: 16px;
padding: 48px 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
text-align: center;
}
.book-icon {
font-size: 48px;
margin-bottom: 16px;
}
h1 {
font-size: 28px;
color: #1a1a2e;
margin-bottom: 8px;
}
.subtitle {
color: #667eea;
font-weight: 600;
margin-bottom: 16px;
}
.description {
color: #555;
line-height: 1.6;
margin-bottom: 32px;
}
form {
display: flex;
flex-direction: column;
gap: 12px;
}
input[type="email"] {
padding: 14px 18px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
outline: none;
}
input[type="email"]:focus {
border-color: #667eea;
}
button {
padding: 14px 18px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #5a6fd6;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.message {
margin-top: 16px;
font-size: 14px;
min-height: 20px;
}
.message.success {
color: #22c55e;
}
.message.error {
color: #ef4444;
}
Add JavaScript to handle form submissions
The form looks good, but clicking the button does nothing yet. You need JavaScript to intercept the form submission, send the email address to your API endpoint, and display a response message to the visitor.
- Create a new file called script.js inside the public folder.
- Paste the following JavaScript into the file:
// Grab references to the form, button, and message elements
const form = document.getElementById('ebook-form');
const submitBtn = document.getElementById('submit-btn');
const message = document.getElementById('message');
// Listen for form submissions
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Read the email value and disable the button while sending
const email = document.getElementById('email').value;
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
message.textContent = '';
message.className = 'message';
try {
// POST the email address to our serverless API endpoint
const response = await fetch('/api/send-ebook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const result = await response.json();
if (response.ok) {
// Show success message and clear the form
message.textContent = 'Check your inbox! The ebook is on its way.';
message.className = 'message success';
form.reset();
} else {
// Show the error returned by the API
message.textContent = result.error || 'Something went wrong. Please try again.';
message.className = 'message error';
}
} catch (error) {
// Handle network failures
message.textContent = 'Network error. Please try again.';
message.className = 'message error';
} finally {
// Re-enable the button regardless of outcome
submitBtn.disabled = false;
submitBtn.textContent = 'Send Me the Ebook';
}
});
What does this script do?
When the form is submitted, e.preventDefault() stops the page from reloading. The script disables the button and shows "Sending..." so the visitor knows something is happening.
The fetch call sends a POST request with the email address to /api/send-ebook. Based on the response, it shows either a green success message or a red error message. The finally block re-enables the button so the visitor can try again if needed.
- Save script.js and refresh your browser.
- Type any email address into the input and click the button.
- You should see the button briefly show "Sending..." and then red text appear that says "Network error. Please try again." below the form.
Why a network error?
This error is expected. The JavaScript is trying to reach your API endpoint at /api/send-ebook, but that endpoint doesn't exist yet. Seeing this error confirms your JavaScript is running correctly and attempting the request. You'll build the actual API in the next step.
No error message appears?
Open your browser's developer console (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for errors. A common issue is the filename not matching. The HTML expects script.js in the same folder as index.html.
help me debug why my form isn't working
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here's the complete script.js for comparison:
const form = document.getElementById('ebook-form');
const submitBtn = document.getElementById('submit-btn');
const message = document.getElementById('message');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value;
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
message.textContent = '';
message.className = 'message';
try {
const response = await fetch('/api/send-ebook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const result = await response.json();
if (response.ok) {
message.textContent = 'Check your inbox! The ebook is on its way.';
message.className = 'message success';
form.reset();
} else {
message.textContent = result.error || 'Something went wrong. Please try again.';
message.className = 'message error';
}
} catch (error) {
message.textContent = 'Network error. Please try again.';
message.className = 'message error';
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Send Me the Ebook';
}
});
Your landing page is built and your JavaScript is ready to talk to the backend. Next up, you'll build the serverless function that receives the email address and sends the ebook as an attachment.
Build the Serverless Function
Your landing page is ready to collect email addresses. Now you need the backend logic that actually delivers the ebook.
When someone submits the form, the JavaScript in script.js sends the email address to /api/send-ebook. You need to create the function that lives at that endpoint. On Vercel, any file inside the api directory automatically becomes a serverless function with its own URL.
In this step, get ready to:
- Create the API route file that handles incoming form submissions.
- Read the PDF ebook and send it as an email attachment using the Resend SDK.
- Add error handling for invalid requests and failed sends.
Create the API route file
Your script.js already sends a POST request to /api/send-ebook when the form is submitted. Vercel maps that URL to a file called api/send-ebook.js in your project.
- In VS Code, create a new file called send-ebook.js inside your api folder.
- Add the following code to api/send-ebook.js:
import { Resend } from 'resend';
import { readFileSync } from 'fs';
import { join } from 'path';
// Initialize Resend client with your API key from environment variables
const resend = new Resend(process.env.RESEND_API_KEY);
export default async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// Extract email from the request body
const { email } = req.body;
// Validate the email address
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Please provide a valid email address.' });
}
try {
// Read the PDF file from the public directory
const pdfPath = join(process.cwd(), 'public', 'ebook.pdf');
const pdfBuffer = readFileSync(pdfPath);
// Send the email with the PDF attached
const { data, error } = await resend.emails.send({
from: 'Ebook Delivery <onboarding@resend.dev>',
to: [email],
subject: 'Your Free Ebook: Master the Basics',
html: `
<h1>Here's your free ebook!</h1>
<p>Thanks for your interest! Your copy of <strong>Master the Basics</strong> is attached to this email as a PDF.</p>
<p>Happy reading!</p>
`,
attachments: [
{
filename: 'master-the-basics.pdf',
content: pdfBuffer,
},
],
});
// Handle Resend API errors
if (error) {
console.error('Resend error:', error);
return res.status(500).json({ error: 'Failed to send email. Please try again.' });
}
// Return success with the email ID
return res.status(200).json({ message: 'Ebook sent successfully!', id: data.id });
} catch (err) {
console.error('Server error:', err);
return res.status(500).json({ error: 'Something went wrong. Please try again.' });
}
}
What does this code do?
This file is split into three logical sections:
- Request validation (lines 8-18): The handler rejects anything that is not a POST request with a 405 status. It also checks that the submitted email contains an @ symbol before proceeding.
- PDF reading and email sending (lines 20-40): readFileSync loads your ebook.pdf into memory as a buffer. Then resend.emails.send() delivers it as an attachment to the submitted email address.
- Error handling (lines 42-50): If the Resend API returns an error, the function responds with a 500 status. A catch block handles any unexpected server errors the same way.
Understand the email sending logic
What is onboarding@resend.dev?
The from field uses onboarding@resend.dev. This is a shared test sender address provided by Resend that works without domain verification. It lets you test the full email flow immediately.
For a production app, you would verify your own domain and use a custom sender address like ebooks@yourdomain.com.
How does the PDF attachment work?
The readFileSync function reads the PDF into a binary buffer in memory. Resend accepts this buffer directly in the attachments array alongside a filename that recipients see in their inbox.
The join(process.cwd(), 'public', 'ebook.pdf') builds the full file path regardless of which directory the function runs from. This ensures Vercel can locate the file in its serverless environment.
Review the error handling
Your function handles three failure scenarios, each returning a JSON error the frontend can display.
What are the three error cases?
- 405 Method Not Allowed: Returned when something other than a POST request hits the endpoint (for example, a browser navigating to the URL directly).
- 400 Bad Request: Returned when the email field is missing or does not contain an @ symbol.
- 500 Internal Server Error: Returned when the Resend API fails or an unexpected error occurs in the try/catch block.
Your frontend script.js already reads the error field from these JSON responses and displays it in the message element below the form.
- Save api/send-ebook.js.
- Confirm the file exists by running:
ls api/
You should see send-ebook.js listed in the output.
Don't see send-ebook.js?
- Make sure you created the file inside the api folder, not in the project root or inside public.
- Check that the filename is exactly send-ebook.js with a hyphen, not an underscore or camelCase.
- help me find where my send-ebook.js file ended up
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete api/send-ebook.js file:
import { Resend } from 'resend';
import { readFileSync } from 'fs';
import { join } from 'path';
const resend = new Resend(process.env.RESEND_API_KEY);
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { email } = req.body;
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Please provide a valid email address.' });
}
try {
const pdfPath = join(process.cwd(), 'public', 'ebook.pdf');
const pdfBuffer = readFileSync(pdfPath);
const { data, error } = await resend.emails.send({
from: 'Ebook Delivery <onboarding@resend.dev>',
to: [email],
subject: 'Your Free Ebook: Master the Basics',
html: `
<h1>Here's your free ebook!</h1>
<p>Thanks for your interest! Your copy of <strong>Master the Basics</strong> is attached to this email as a PDF.</p>
<p>Happy reading!</p>
`,
attachments: [
{
filename: 'master-the-basics.pdf',
content: pdfBuffer,
},
],
});
if (error) {
console.error('Resend error:', error);
return res.status(500).json({ error: 'Failed to send email. Please try again.' });
}
return res.status(200).json({ message: 'Ebook sent successfully!', id: data.id });
} catch (err) {
console.error('Server error:', err);
return res.status(500).json({ error: 'Something went wrong. Please try again.' });
}
}
Your serverless function is ready. Next up, you will configure the project for deployment and push it live to Vercel so anyone can visit the page and request the ebook.
Configure and Deploy to Vercel
Your landing page, styling, JavaScript, and serverless function are all ready. The final piece is getting everything live on the internet.
In this step, you will configure Vercel to correctly route requests between your static files and API function, securely store your API key, and deploy the entire project to a public URL.
In this step, get ready to:
- Create a routing configuration file for Vercel.
- Add your Resend API key as a secure environment variable.
- Deploy and test your live ebook delivery system.
Create the Vercel configuration file
Vercel needs to know how to route incoming requests. Static files live in the public/ directory, and API requests go to the api/ directory.
A vercel.json file at the project root tells Vercel exactly where to send each request.
- Create a new file called vercel.json in the root of your ebook-landing-page folder (at the same level as package.json).
- Paste the following configuration into vercel.json:
{
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api/$1" },
{ "source": "/(.*)", "destination": "/public/$1" }
]
}
What does this configuration do?
The rewrites array tells Vercel how to map URLs to files in your project.
- The first rule sends any request starting with /api/ to your serverless functions in the api/ directory.
- The second rule sends all other requests to files in the public/ directory (your HTML, CSS, and JavaScript).
- Save vercel.json.
- Confirm the file exists by running:
ls
You should see vercel.json listed alongside package.json, api/, and public/.
Don't see vercel.json?
Make sure you created the file in the project root (ebook-landing-page/), not inside the public/ or api/ folders.
help me figure out where my vercel.json should go
✔️ Awesome, I've got everything!
Great. Double check you have saved vercel.json.
ⓧ I'd like to double check the full code
{
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api/$1" },
{ "source": "/(.*)", "destination": "/public/$1" }
]
}
Add your Resend API key as an environment variable
Your serverless function reads the API key from process.env.RESEND_API_KEY. You need to store this value securely on Vercel so the deployed function can access it.
The Vercel CLI lets you add environment variables directly from your terminal.
- Log in to your Vercel account by running:
vercel login
What happens when you log in?
This command opens your browser and asks you to confirm the login. Once confirmed, your terminal is authenticated with your Vercel account.
- Link your local project to Vercel by running:
vercel
What will the CLI ask you?
Since this is your first time deploying this project, the CLI will ask setup questions. Answer them as follows:
- "Set up and deploy?" - enter Y.
- "Which scope?" - select your personal account.
- "Link to existing project?" - enter N.
- "Project name?" - press Enter to accept the default.
- "In which directory is your code located?" - press Enter to accept ./.
- "Want to override the settings?" - enter N.
This creates a preview deployment and links your project. You will see a deployment URL in the terminal output.
- Now add your Resend API key as an environment variable by running:
vercel env add RESEND_API_KEY
What does this command do?
The CLI will prompt you to select which environments to apply the variable to. Select all environments (Production, Preview, and Development).
Then it will ask you to paste the value. Paste the Resend API key you saved from Step 1 and press Enter.
Getting a "not linked" error?
Make sure you ran vercel first to link the project. The vercel env add command needs a linked project to know where to store the variable.
help me fix a Vercel CLI not linked error
Deploy to production and test
Your environment variable is set, but it was added after the initial deployment. You need to redeploy for the variable to take effect.
- Deploy your project to production by running:
vercel --prod
Once the deployment finishes, the CLI prints your live production URL. It looks something like ebook-landing-page.vercel.app.
- Open the production URL in your browser.
- You should see your ebook landing page with the purple gradient background, the card, and the email form.
- Enter your email address in the form and click Send Me the Ebook.
- Check your inbox. You should receive an email from "Ebook Delivery" with the subject "Your Free Ebook: Master the Basics" and an attached PDF.
Using the Resend test sender?
The onboarding@resend.dev sender address works for testing without domain verification. On Resend's free plan, you can only send to email addresses associated with your Resend account unless you verify a custom domain.
If you need to test with a different recipient, verify your own domain in the Resend dashboard first.
Form submits but no email arrives?
- Check your spam or junk folder.
- Make sure you added RESEND_API_KEY to all environments and redeployed with vercel --prod after adding it.
- Confirm the recipient email is associated with your Resend account (required on the free plan without a verified domain).
help me debug why my Vercel deployed function is not sending emails
Your ebook delivery system is live. Anyone with the URL can enter their email and instantly receive the PDF. Next up, you can extend this with a thank you page and download counter in the Secret Mission.
Secret mission
Add a Download Counter and Thank You Page
After someone requests your ebook, they see a generic success message. What if you could redirect them to a polished thank you page that also shows how many ebooks have been delivered? Build a confirmation page with a live counter to add personality and social proof to your delivery system.
Clean Up Your Resources
Clean Up Your Resources
This project runs on Vercel's free Hobby plan and Resend's free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local project folder (ebook-landing-page).
- Vercel project (deployed serverless function and static site).
- Vercel environment variable (RESEND_API_KEY).
- Resend API key.
- Vercel CLI (installed globally via npm).
Keep everything running
No action needed. Choose this if you plan to keep building on the project or extend it with new features.
- Your Vercel deployment stays live at its URL.
- Your Resend API key remains active for sending emails.
- The Vercel CLI stays installed on your machine for future projects.
Pause - I'll come back to this later
Both Vercel and Resend are free tier, so there is nothing to pause. Your deployment and API key stay active at no cost until you decide to delete them.
- Your Vercel deployment remains live with no charges on the Hobby plan.
- Your Resend API key stays valid but unused.
- You can come back and redeploy anytime by running the Vercel CLI from your project folder.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Delete your Vercel project by opening the Vercel dashboard, navigating to your project, clicking Settings, scrolling to the bottom of the General page, and clicking Delete Project. This removes all deployments, domains, and environment variables associated with the project.
- Delete your Resend API key by opening the Resend dashboard, clicking API Keys in the sidebar, clicking the options menu next to your key, and selecting the delete option.
- Uninstall the Vercel CLI by running:
npm uninstall -g vercel
- Delete your local project folder on macOS or Linux by running:
rm -rf ~/Desktop/ebook-landing-page
On Windows?
Use this command instead to remove the project folder:
rmdir /s /q %USERPROFILE%\Desktop\ebook-landing-page
That's a Wrap!
That's a Wrap!
Nice work! You just built and deployed a fully automated ebook delivery system. Anyone with the link can enter their email and receive your PDF straight to their inbox.
You've learned how to:
- Build a live landing page with an email capture form that sends data to a serverless API endpoint.
- Use the Resend SDK to send emails with PDF attachments programmatically from a Node.js function.
- Deploy a full-stack project to Vercel with environment variables and a production URL accessible from anywhere.
- Secret Mission: Add a thank you page with a live download counter that tracks how many ebooks have been sent.
Ready to quiz yourself?