Objective: PDF Generation with Serverless Functions
Run puppeteer
on Vercel's Serverless Functions
and, at the same time a generate PDF.
Understanding the Error 500: The Limitations of Chromium on AWS Lambda
Running puppeteer
on Vercel will return an Error 500
because puppeteer
relies on a version of Chromium
which is too large to be included on a AWS Lambda.
Optimizing Puppeteer for Vercel: A Lightweight Solution
Vercel's Serverless Functions
are powered by AWS Lambdas, which have a 50MB limit on the deployment package size. Chromium, which is required by Puppeteer, is too large to be included in our Lambda function.
In order for Puppeteer to be included, a smaller version of Chromium is required.
puppeteer-core
instead ofpuppeteer
onproduction
@sparticuz/chromium
.
The Solution
Getting Started: Essential Packages for Serverless PDF Creation
# Puppeteer
pnpm add @sparticuz/chromium@123.0.1
pnpm add puppeteer-core@21.9.0
pnpm add -D puppeteer@21.9.0
# Implement a PDF
pnpm add @onedoc/react-print
Configuration: Setting Up Puppeteer-Core with Custom Chromium
I have created a Server Action, but feel free to use it on a route handler.
export async function makdePDF(html: string) {
// Initiate the browser instance
let browser: Browser | undefined | null;
// Check if the environment is development
if (process.env.NODE_ENV !== "development") {
// Import the packages required on production
const chromium = require("@sparticuz/chromium");
const puppeteer = require("puppeteer-core");
// Assign the browser instance
browser = await puppeteer.launch({
executablePath: await chromium.executablePath(),
headless: chromium.headless,
ignoreHTTPSErrors: true,
defaultViewport: chromium.defaultViewport,
args: [...chromium.args, "--hide-scrollbars", "--disable-web-security"],
});
} else {
// Else, use the full version of puppeteer
const puppeteer = require("puppeteer");
browser = await puppeteer.launch({
headless: "new",
});
}
// Create a PDF
if (browser) {
const page = await browser.newPage();
await page.setContent(html);
const pdfBuffer = await page.pdf({
format: "a4",
printBackground: true,
margin: {
top: 80,
bottom: 80,
left: 80,
right: 80,
},
});
await browser.close();
return pdfBuffer;
}
return null;
}
Crafting PDFs: Serverless Document Creation
Follow the tutorial on React Print.
export function PdfCv() {
return (
<Tailwind>
<div className="p-2 md:p-6">
<p className="text-xs font-semibold leading-7 text-indigo-600">
{cv.title}
</p>
<h1 className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
{cv.name}
</h1>
// ...
</div>
</Tailwind>
);
}
Facilitating Downloads: Building a Route for Serverless PDF Downloads
In order to make the PDF available for download, a route handler is required to be created with the appropriate headers. I personally created the route handler in the /app
directory following the below structure:
- /app
- /cv
- page.tsx
- /download
- route.tsx
The following snippet is the route handler for downloading the PDF:
// /download/route.tsx
import { compile } from "@onedoc/react-print";
import { PdfCv } from "../_components/pdf-cv";
import { makePDF } from "../actions/make-pdf";
import postcss from "postcss";
import theme from "../../../tailwind.config";
import path from "path";
import fs from "fs";
async function importTailwindGlobals() {
const cssPath = path.join(__dirname, "../../../../../app/globals.css");
try {
const cssContent = fs.readFileSync(cssPath, "utf8");
return cssContent;
} catch (error) {
console.error("Error reading the Tailwind Globals css file:", error);
}
}
export async function GET(request: Request) {
try {
// Create a new header
const headers = new Headers();
// Compile tailwind css
const css = await postcss([
require("tailwindcss")({
...theme,
corePlugins: { preFlight: false },
}),
])
.process(`${globals}`, { from: undefined })
.then((result) => result.css);
// Compile the PDF
const html = await compile(
<div>
<style dangerouslySetInnerHTML={{ __html: css }} />
<PdfCv mode={mode} />
</div>
);
// Create a PDF buffer
const pdfBuffer = await makePDF(html);
// Assign PDF header as content-type
headers.append("Content-Type", "application/pdf");
// Assign the disposition header as attachment
headers.append(
"Content-Disposition",
`attachment; filename="nabil-benhamou-resume-${new Date().getFullYear()}.pdf"`
);
// Return the PDF buffer
return new Response(pdfBuffer, { headers });
} catch (error) {
return new Response("Error downloading PDF", {
status: 500,
headers: {
"Content-Type": "text/plain",
},
});
}
}
Nextjs Configuration: Exclude Puppeteer and Chromium
Because puppeteer-core
and @sparticuz/chromium
are used inside Server Components and Route Handlers they will automatically be bundled by Next.js.
Therefore they require to be marked as external
in the RSC
server build.
const nextConfig = {
// ...
serverExternalPackages: ["puppeteer-core", "@sparticuz/chromium"],
// ...
};