How to Use Puppeteer on Vercel with Next.js

Introduction

In this tutorial, we will build a Next.js application that generates PDF files using the Headless Chrome API Puppeteer. Puppeteer will be utilized as a serverless function, and the app will be deployed on Vercel.

Table of Contents

  • Project Setup
  • Puppeteer
  • Deployment

Project Setup

First, let's create a new Next.js application. For simplicity, we will use JavaScript. Open your terminal and run the following command:

npx create-next-app@latest

You should receive a similar output to this. In my case, I named the project next-puppeteer-demo-app:

npx create-next-app@latest
Need to install the following packages:
    create-next-app@latest
Ok to proceed? (y) y
What is your project named? ... next-puppeteer-demo-app

Navigate to your project directory and run:

npm run dev

This command will start the application, and you should be able to view it in your browser using the address localhost:3000.

We will remove the Home.module.css file located inside the styles folder. Additionally, we will remove some content inside pages/index.js. So that it looks like this:

export default function Home() {
  return (
    <div>
      <main>{/* content will be placed here */}</main>
    </div>
  );
}

Styling the web application is not the focus of this tutorial.

Now rename the hello.js file located in pages/api to generatePdf.js. Files inside pages/api function as your backend; each file is essentially a serverless function. Later on, we will call this function from our frontend, triggering a serverless function that generates a PDF file.

Your generatePdf.js should look like this for now:

export default function handler(req, res) {
  res.status(200).json({ message: "Generate PDF!" });
}

Puppeteer

We are going to install puppeteer via npm, but we are not going to install the full puppeteer package since that won't work. The maximum size for a Serverless Function is 50 MB, by using the puppeteer package we exceed that size which causes an error.

We are going to install chrome-aws-lambda which is a Chromium Binary for AWS Lambda and Google Cloud Functions

npm install chrome-aws-lambda --save-prod
npm install playwright-core

Next, update your generatePdf.js file. If you want to test the application locally, you need to install Chromium on your local development machine and pass the location to the ternary operation.

On macOS, you can install Chromium using Homebrew:

brew install --cask chromium

Your updated generatePdf.js should look like this:

import chromium from "chrome-aws-lambda";
import playwright from "playwright-core";

export default async function generatePdf(req, res) {
  try {
    const browser = await playwright.chromium.launch({
      args: [...chromium.args, "--font-render-hinting=none"], // This way fix rendering issues with specific fonts
      executablePath:
        process.env.NODE_ENV === "production"
          ? await chromium.executablePath
          : "/usr/local/bin/chromium",
      headless:
        process.env.NODE_ENV === "production" ? chromium.headless : true,
    });

    const context = await browser.newContext();

    const page = await context.newPage();

    // This is the path of the url which shall be converted to a pdf file
    const pdfUrl =
      process.env.NODE_ENV === "production"
        ? "https://your.app/pdf"
        : "http://localhost:3000/pdf";

    await page.goto(pdfUrl, {
      waitUntil: "load",
    });

    const pdf = await page.pdf({
      path: "/tmp/awesome.pdf", // we need to move the pdf to the tmp folder otherwise it won't work properly
      printBackground: true,
      format: "a4",
    });
    await browser.close();

    return res.status(200).json({ pdf });
  } catch (error) {
    return res.status(error.statusCode || 500).json({ error: error.message });
  }
}

This method starts Chromium with specific arguments. Once Chromium is started, it navigates to the specified URL and waits until it's fully loaded. Then, it converts the page to a PDF file, saving it in a temporary file location. Finally, it closes the browser and returns the generated PDF file.

Now, update your frontend. Create a new file named pdf.js inside the pages directory. In this file, you can create a custom view. In our case, we'll simply return a string, but you can style it as desired:

const Pdf = () => {
  return (
    <div>
      <h1>This page is going to be converted to a pdf, wow!</h1>
    </div>
  );
};

export default Pdf;

Next, open the index.js file and add a button that will call our backend method. We'll use fetch to call the API:

export default function Home() {
  const generatePdf = async () => {
    const res = await fetch("/api/generatePdf", {
      headers: {
        "Content-Type": "application/json",
      },
      method: "POST", // You probably can use get aswell, you can use a post request with a custom body to generate dynamic data in your pdf view, I am going to cover that in a different post :)
    });
    return res.json();
  };

  return (
    <div>
      <main>
        <button type="button" onClick={generatePdf}>
          Create PDF
        </button>
      </main>
    </div>
  );
}

Now you can open your application locally and click the button to generate a PDF file. The file will be automatically downloaded by your browser.

Deployment

We will deploy the application on Vercel. Visit vercel.com, register an account or sign in to an existing account, and create a git repository. Push your project to that repository (you can use GitHub or GitLab).

Once you've pushed your code, go to vercel.com, click on "New Project," search for your project, and import it. Skip the "Create a Team" section and click on "Deploy."

The deployment/build process should take only a couple of minutes. Once the project is successfully deployed, you can test it using the production URL provided by Vercel (yourawesomeapp.vercel.app).

Cheers!