How to generate PDF's with Puppeteer on Vercel in 2024
Tags: vercel, puppeteer, serverless, nextjs
Published: 2024-05-09 19:47:17

I've spent a lot of time gathering information, reading documentation, and banging my head against the wall to get this to work finally, so I thought I'd share my setup for anyone else chewing on this problem.

We're using `@sparticuz/chromium` to provide the chromium executable bundle to `puppeteer`. This alone wasn't enough to get it to work, as it continually timed out attempting to create a new page until I set the args specifically to this:

```js
  const chromeArgs = [
    '--font-render-hinting=none', // Improves font-rendering quality and spacing
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-gpu',
    '--disable-dev-shm-usage',
    '--disable-accelerated-2d-canvas',
    '--disable-animations',
    '--disable-background-timer-throttling',
    '--disable-restore-session-state',
    '--disable-web-security', // Only if necessary, be cautious with security implications
    '--single-process', // Be cautious as this can affect stability in some environments
  ];

```

This solution also attempts to keep the browser instance around between cold starts, but I haven't thoroughly tested that to see if it helps. 

Also, keep in mind that I am running this on my own site with my own user's generated content, so I don't have any real security concerns regarding the Chromium args. If you're running this on public sites, you may need to further investigate whether these settings will work for you.

Here is the full code:

```ts

//Page router vercel function config
export const config = {
  maxDuration: 150,// seconds. You'll need a paid plan to increase the duration above 10s as it's likely not enough time to complete the task. 
};

//App router vercel function config
//export const maxDuration = 150; 

import chromium from '@sparticuz/chromium';
import puppeteer, { Browser } from 'puppeteer';

let browser: Browser;
const isLocal = process.env.NODE_ENV === 'development';


const generatePDF = async ({ url }: { url: string }) => {

  // Chromium only ships with Open Sans font by default
  // You can load additional fonts using the `font` method
  // Load a font from a local file

  // await chromium.font("/var/task/fonts/NotoColorEmoji.ttf");

  // or

  // await chromium.font(
  //   "https://raw.githack.com/googlei18n/noto-emoji/master/fonts/NotoColorEmoji.ttf"
  // );


  chromium.setHeadlessMode = true;
  chromium.setGraphicsMode = false;

  const chromeArgs = [
    '--font-render-hinting=none', // Improves font-rendering quality and spacing
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-gpu',
    '--disable-dev-shm-usage',
    '--disable-accelerated-2d-canvas',
    '--disable-animations',
    '--disable-background-timer-throttling',
    '--disable-restore-session-state',
    '--disable-web-security', // Only if necessary, be cautious with security implications
    '--single-process', // Be cautious as this can affect stability in some environments
  ];


  try {
    if (!browser?.connected) {
      browser = await puppeteer.launch({
        ...(isLocal
          ? { channel: 'chrome' }
          : {
            args: chromeArgs,
            executablePath: await chromium.executablePath(),
            ignoreHTTPSErrors: true,
            headless: true,
          }),
      });
    }
  } catch (error) {
    throw new Error('Failed to start browser');
  }


  const page = await browser.newPage();

  await page.emulateMediaType('print')

  const response = await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });

  if (!response || !response.ok()) {
    throw new Error('Failed to load the page for PDF generation');
  }

  const pdf = await page.pdf({
    format: 'a4',
    omitBackground: true, // Omit background colors and images
    printBackground: true, // Include background graphics
  });


  const pages = await browser.pages();
  for (const openPage of pages) {
    await openPage.close(); // Close all open pages to avoid resource leaks
  }


  if (isLocal) {
    await browser.close(); // Close the browser if running locally
  }

  return pdf;
};
```