ScreenshotNeo

BlogHTML to image & PDF

How to Generate PDFs with Next.js on Vercel Without Running Chrome

Generate PDFs in a Next.js Route Handler on Vercel with React-PDF, or use a hosted renderer when your document depends on browser HTML and CSS.

By the ScreenshotNeo team29 September 202610 min read

How to Generate PDFs with Next.js on Vercel Without Running Chrome

You can generate PDFs in a Next.js app on Vercel without launching Chrome. For documents you can build from PDF-specific React components, use @react-pdf/renderer in a server-side Route Handler configured for the Node.js runtime. It renders through its own document components and layout system; it does not reproduce arbitrary browser HTML and CSS. If your PDF must look like an existing web page, use a hosted browser renderer instead of trying to make a PDF layout library behave like Chrome. React-PDF’s documentation covers its server rendering and stream API.

This guide uses the Next.js App Router. It keeps generation on the server, returns a streamed PDF response, and covers the Vercel constraints that matter at deployment. Check your installed Next.js and React-PDF versions before shipping: the example illustrates the documented server-rendering direction, but the research for this article did not test a specific project’s dependency combination or deploy it.

1. Choose a renderer that matches the document

Choose based on where the layout already lives:

Approach Use it when Trade-off
@react-pdf/renderer in a Node.js Route Handler You can compose the document from React-PDF’s components and styles. You author a PDF layout in its own model; browser HTML/CSS is not automatically reproduced.
Hosted PDF rendering API You need to render existing HTML/CSS or prefer not to bundle and operate a renderer. A service becomes part of your request path. Validate fidelity, data handling, latency, pricing, limits, and reliability against your workload.

The React-PDF v4 documentation describes server rendering and renderToStream. Vercel documents Node.js as its runtime with full Node API coverage; Edge offers a restricted API set. That makes Node.js the sensible starting point for a Node-dependent renderer. Always check the package’s actual dependencies and compatibility before selecting a runtime. React-PDF documentation · Vercel Node.js runtime · Vercel Edge runtime

2. Install the renderer

From the Next.js project root, install React-PDF:

npm install @react-pdf/renderer

Keep the package in production dependencies because the Route Handler imports it at runtime. Use a Node.js version supported by your project and the installed package. If your app uses a different package manager, use its equivalent install command and commit the lockfile.

3. Define a PDF document

React-PDF documents use its primitives such as Document, Page, View and Text. This small example defines an invoice-style document. In a real app, pass validated server-side data into the component and format it before rendering.

// app/api/invoice/document.tsx
import { Document, Page, StyleSheet, Text, View } from '@react-pdf/renderer';

const styles = StyleSheet.create({
  page: { padding: 48, fontSize: 12 },
  heading: { fontSize: 22, marginBottom: 20 },
  row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 },
});

type InvoiceDocumentProps = {
  invoiceNumber: string;
  customer: string;
  amount: string;
};

export function InvoiceDocument({ invoiceNumber, customer, amount }: InvoiceDocumentProps) {
  return (
    <Document title={`Invoice ${invoiceNumber}`}>
      <Page size="A4" style={styles.page}>
        <Text style={styles.heading}>Invoice {invoiceNumber}</Text>
        <View style={styles.row}>
          <Text>Customer</Text>
          <Text>{customer}</Text>
        </View>
        <View style={styles.row}>
          <Text>Total</Text>
          <Text>{amount}</Text>
        </View>
      </Page>
    </Document>
  );
}

The escaped angle brackets above are HTML encoding for JSX inside this article. In your .tsx source file, use normal JSX delimiters, for example <Document> as literal source syntax. Add pages, fonts, images, and styles using the renderer’s documented APIs. Avoid assuming CSS properties or browser layout features work the same way; React-PDF has its own layout model.

4. Return the PDF from a Next.js Route Handler

Place this route at app/api/invoice/route.tsx. Rendering runs on the server, and the response uses application/pdf. This example calls renderToStream and adapts its Node stream for the Web Response API.

A Node.js Route Handler can render a React-PDF document on the server and stream the resulting PDF response.
A Node.js Route Handler can render a React-PDF document on the server and stream the resulting PDF response.
// app/api/invoice/route.tsx
import { Readable } from 'node:stream';
import { renderToStream } from '@react-pdf/renderer';
import { InvoiceDocument } from './document';

export const runtime = 'nodejs';
export const maxDuration = 30;

export async function GET() {
  const pdf = await renderToStream(
    <InvoiceDocument
      invoiceNumber="INV-1042"
      customer="Example Customer"
      amount="$125.00"
    />,
  );

  const webStream = Readable.toWeb(pdf as Readable) as ReadableStream;
  return new Response(webStream, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'inline; filename="invoice-INV-1042.pdf"',
      'Cache-Control': 'private, no-store',
    },
  });
}

As with the document example, replace HTML-escaped JSX delimiters with ordinary delimiters in the TypeScript file. The route is a starting pattern: check the installed React-PDF types, your Next.js version’s Route Handler requirements, and the stream type returned by your package version. If the type does not match Readable.toWeb, adapt it according to the installed API rather than suppressing unrelated type errors.

Open /api/invoice locally to view or download the PDF. For production, authenticate the request, load the invoice by an authorized identifier, validate access to that record, and pass only the required fields to the document. The sample’s fixed values make the route easy to understand; exposing real invoices without authorization would expose private data.

5. Handle data, output, and caching deliberately

Validate before rendering

Reject missing or malformed identifiers before querying data or constructing the document. Return a suitable error response for invalid input or missing records. Do not return internal exception messages, database details, or secrets to callers. If rendering can fail partway through, log a request identifier and the server-side error so an operator can investigate without exposing document contents.

Choose inline or attachment

Content-Disposition: inline asks browsers to display the PDF where possible; attachment; filename="invoice.pdf" asks them to download it. Use a filename derived from trusted data and strip characters that could produce an invalid header. Keep Content-Type: application/pdf. For sensitive documents, use private cache controls and ensure any CDN or application cache respects authorization.

Streaming and buffering

A stream can send output progressively and avoid building the complete response buffer in application memory. That does not make document creation free or guarantee that the first bytes arrive immediately: layout, images, fonts, and document size affect work. If you choose a buffer API instead, the complete PDF occupies memory until it can be returned. Measure representative documents under your actual function configuration.

Fonts and assets

Fonts and bundled files contribute to the deployed function size. Use only the fonts and assets the document needs, and verify that they are included in the Vercel function bundle. Prefer controlled, server-accessible asset sources. Remote asset fetches add latency and can fail; do not let user-controlled URLs turn document generation into an unrestricted server-side fetch.

6. Deploy on Vercel’s Node.js runtime

Vercel documents a 250 MB maximum uncompressed size for a Node.js function, including its code, dependencies, and bundled files. Memory and duration vary with plan and configuration. These are platform limits, not PDF performance promises. Inspect the deployed function and confirm its actual size and settings. Vercel function limits

  1. Keep runtime = 'nodejs' on the route. Do not set this renderer route to Edge without confirming every dependency works in Edge’s limited API environment.
  2. Deploy a preview and inspect the function bundle size. Remove unused dependencies and unnecessary font or asset files if packaging approaches the ceiling.
  3. Generate small, typical, and largest-expected documents. Check duration and memory in the project’s real plan and configuration.
  4. Set maxDuration only to a value permitted by your plan. A larger configured limit does not reduce render time or guarantee a successful response.
  5. Use /tmp only when temporary scratch files are necessary. Vercel documents a read-only function filesystem with writable /tmp space up to 500 MB; it is temporary storage, not durable storage. Vercel runtimes and filesystem

Edge functions have different constraints. Vercel documents a 128 MB maximum memory and a requirement to begin sending a response within 25 seconds to continue streaming beyond that point. It is not a safe assumption that a Node-dependent PDF renderer will work there. Edge runtime limits

7. When the PDF must match HTML and CSS

If the source of truth is a page template and you need browser HTML/CSS rendering, evaluate a hosted PDF rendering API. PDFgen and PDF4.dev describe remote rendering services callable from an application; those are vendor claims, not independent verification of fidelity or operational quality. Test representative pages, including long content, page breaks, custom fonts, and images. Review data handling, service limits, latency, pricing, and reliability before depending on the service. PDFgen · PDF4.dev

React-PDF has its own layout model; browser rendering is the better fit when HTML and CSS fidelity is required.
React-PDF has its own layout model; browser rendering is the better fit when HTML and CSS fidelity is required.

For a PDF of a public web page, ScreenshotNeo is a hosted option from Yorker Media. It is a website screenshot API and MCP server; its API can return a PDF as well as PNG, JPEG, or WebP. Its capture renders a page in a browser, so it shifts that browser work to the service rather than making the rendering browser-free. See ScreenshotNeo and its API documentation for request options and PDF configuration.

Or skip the browser setup

For a URL-based capture, one GET request can return a PDF. Keep your access key on the server and replace the example URL with the page you want to capture. See the ScreenshotNeo docs for PDF options and request parameters.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o page.pdf

ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Free includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Sign up free for 1,000 screenshots a month, no card required.

8. Troubleshooting

Symptom Likely cause What to check
Build or runtime error mentions a missing Node API The route was configured for Edge, or a dependency is incompatible with the runtime. Set the route to nodejs; inspect the full dependency chain and package compatibility.
Function bundle exceeds the limit Renderer dependencies, fonts, or included files make the function too large. Inspect the bundle, remove unused imports/assets, and confirm the packaged uncompressed size against Vercel’s current limits.
PDF route times out The document is expensive to lay out, assets are slow, or the configured function duration is too low for the plan. Try a representative document, reduce unnecessary assets, avoid slow external fetches, and verify the function duration setting and plan ceiling.
PDF is blank or missing content Input data may be empty, the component tree may not contain the expected primitives, or an asset may not load. Log safe metadata about inputs, check rendering errors, and test the document component with fixed data.
Layout differs from the website React-PDF uses its own primitives and styles rather than general browser HTML/CSS. Build the layout in React-PDF’s model or use a browser-based hosted renderer when page fidelity is required.
TypeScript rejects the stream conversion Stream types vary by library or runtime version. Check the installed renderToStream type and Node stream declarations; adapt the conversion for that version rather than adding a broad cast to unrelated code.
Document leaks between users through caching A personalized response is cached publicly or its cache key omits the user/document identity. Use private no-store behavior for sensitive PDFs and verify CDN and application caching.

9. Performance, reliability, and cost

For a React-PDF route, cost and capacity depend on your Vercel plan, function duration, memory configuration, and request volume. The available research provides no neutral benchmark comparing React-PDF with hosted rendering services. Measure render duration and memory using realistic PDFs in your own deployment. Large images, many pages, complex layouts, and font files can increase work or packaging size; constrain input size and page complexity where users supply data.

Keep document generation deterministic where possible: pin dependencies with a lockfile, control fonts and assets, and handle failures with useful server logs and safe client responses. For large or bursty jobs, a synchronous request may exceed your response window; design a background job flow if the workload requires it, and confirm that approach against your hosting plan and chosen service. A hosted API moves rendering operations outside your function but adds a network and vendor dependency, so assess timeouts and retry behavior without duplicating a chargeable request blindly.

FAQ

Can I use React-PDF with a Next.js App Router route?

The documented approach is server rendering in a Route Handler on Node.js. Confirm compatibility across your installed Next.js, React, and React-PDF versions.

Does React-PDF convert an existing React page into a PDF?

It creates a PDF from its own document components and layout system. It is not general browser HTML/CSS conversion.

Should I write the PDF to disk first?

Usually a response stream avoids needing a temporary file. If your workflow needs scratch files, Vercel provides writable /tmp space up to 500 MB, which is temporary.

Can ScreenshotNeo produce PDFs from a Next.js page?

It can capture a URL as a PDF. Use a publicly reachable page or a URL and access pattern supported by its API documentation; keep credentials server-side.

Decision: use React-PDF on Node.js when the document can be authored in its PDF-specific layout model. If the requirement is an existing page rendered as it appears in a browser, evaluate a hosted browser-based PDF renderer and validate it with your real pages.