Add a PDF download to an existing Next.js page, or generate a PDF from a template. This guide uses PDFCrowd's HTML to PDF API from App Router route handlers, keeping conversion on the server and React responsible for the page.
Set up the client
Install the Node.js client in your Next.js application. The examples use Next's default Node runtime and the async/await interface available in SDK 7 and later:
npm install pdfcrowd
You can run the examples below with the included demo credentials, or replace them with your
PDFCrowd username and API key.
Add a PDF download to an existing page
Keep the page's normal rendering, data preparation, and access checks. A download form sends a POST to a separate route, which asks PDFCrowd to open the page's URL and returns the resulting PDF.
The page and its assets must be reachable from PDFCrowd. For documents that do not have a publicly reachable page, use the template approach below.
Add the download form
Suppose your page at app/documents/[documentId]/page.tsx displays a document with a
numeric id. Configure the public application URL in lib/pdf-config.ts:
export const publicBaseUrl = 'https://your-site.example/';
Replace the URL with your own, including its trailing slash. If your application uses a
base path, include it here, for example https://your-site.example/portal/.
Import publicBaseUrl from @/lib/pdf-config in the page and add this form outside any
existing form:
<form method="post" action={new URL(`documents/${document.id}/pdf`, publicBaseUrl).pathname} className="pdfcrowd-remove" > <input type="hidden" name="csrf_token" value={csrfToken} /> <button type="submit">Download PDF</button> </form>
Here, csrfToken represents your application's CSRF token. Use its actual field name and
token generation. The handler below must validate the same token. The form action keeps
the configured base path, and pdfcrowd-remove hides the form in the PDF.
Return the PDF
Create app/documents/[documentId]/pdf/route.ts. A page and a route handler cannot share
the same route, so this endpoint sits beneath the existing document page.
The imported getDocumentForCurrentUser(request, documentId) is a placeholder for your
document lookup and permission checks; it should return null for missing or inaccessible
documents. verifyCsrfToken(request, form) represents your existing POST protection and
returns whether the request is valid. Replace both imports with your application's logic,
retaining its authentication checks.
import { HtmlToPdfClient } from 'pdfcrowd'; import { publicBaseUrl } from '@/lib/pdf-config'; import { getDocumentForCurrentUser } from '@/lib/documents'; import { verifyCsrfToken } from '@/lib/csrf'; export async function POST( request: Request, { params }: { params: Promise<{ documentId: string }> }, ) { try { const form = await request.formData(); if (!await verifyCsrfToken(request, form)) { return new Response('Forbidden', { status: 403 }); } const { documentId } = await params; if (!/^\d+$/.test(documentId)) { return new Response('Not found', { status: 404 }); } const document = await getDocumentForCurrentUser(request, documentId); if (!document) { return new Response('Not found', { status: 404 }); } const url = new URL(`documents/${documentId}`, publicBaseUrl); const client = new HtmlToPdfClient('demo', 'demo'); client.setContentViewportWidth('balanced'); const pdf = await client.convertUrl(url.href); // Buffer return new Response(new Uint8Array(pdf), { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="document-${documentId}.pdf"`, }, }); } catch (error) { // Add your application's error handling and logging here. return new Response('PDF generation failed', { status: 500 }); } }
The handler creates a client for each conversion and waits for the complete PDF Buffer.
Uint8Array supplies its binary bytes to the Web Response API. Conversion failures reach
the catch block before any PDF headers or bytes are sent. Keep SDK details in server logs;
do not use SDK error codes as HTTP response statuses. See
SDK error handling for details.
The page URL comes from your configured base URL and the validated document ID. If the page
uses query parameters, add only the values your application needs through
url.searchParams; the handler does not forward arbitrary URLs or request parameters.
To display the PDF in the browser, change attachment to inline. Set
page size,
margins, or other
PDF options before convertUrl().
What PDFCrowd loads
PDFCrowd makes a separate GET request to the document page, which renders through Next.js as usual. That GET does not run the POST download handler. The application must be able to serve the page while the download request waits for conversion.
The separate request does not inherit the browser's session. For protected pages, configure cookies or HTTP authentication as appropriate, or generate a PDF from the authorized data using the template approach below.
Stylesheets, images, fonts, and scripts used by the page must also be reachable from
PDFCrowd. It cannot access your development machine through localhost or 127.0.0.1.
URL conversion opens a fresh page, so it does not capture unsaved changes in the user's
browser. For that use case, see PDF export in reactive apps
with WebSave as PDF.
Generate a PDF from a template
You can generate a PDF from a template and application data without an existing HTML page. This example uses EJS to produce document HTML separately from Next's React page rendering. You can substitute an HTML renderer your application already uses.
Install EJS and the marker used to keep the helper in server code:
npm install ejs server-only npm install --save-dev @types/ejs
Put a document template in lib/document-template.ts:
export const documentTemplate = `<!doctype html> <html> <head> <meta charset="utf-8"> <base href="<%= publicBaseUrl %>"> <title><%= document.title %></title> </head> <body> <h1><%= document.title %></h1> <p><%= document.body %></p> </body> </html>`;
The <%= tags escape inserted values as HTML. Keeping the template in a module includes it
in the application build; no template file needs to be loaded from disk at runtime.
Create lib/render-pdf.ts:
import 'server-only'; import ejs from 'ejs'; import { HtmlToPdfClient } from 'pdfcrowd'; export async function renderPdf( template: string, context: Record<string, unknown>, ): Promise<Buffer> { const html = ejs.render(template, context); const client = new HtmlToPdfClient('demo', 'demo'); client.setContentViewportWidth('balanced'); return client.convertString(html); }
The server-only import makes Next reject accidental imports of this helper into Client
Components. Call it from an async server function with your document data:
import { documentTemplate } from '@/lib/document-template'; import { publicBaseUrl } from '@/lib/pdf-config'; import { renderPdf } from '@/lib/render-pdf'; // Inside an async server function, after loading and authorizing the document: try { const pdf = await renderPdf(documentTemplate, { document, publicBaseUrl }); // Buffer // Use the PDF for a download, storage, or email. } catch (error) { // Add your application's error handling here. throw error; }
The helper sends no HTTP response and does not read request state. Rendering or conversion failures reject its Promise. Choose the template and context keys in application code; do not use user-supplied template source or pass request parameters directly as render options.
Make CSS and images available
The template's <base> uses the same publicBaseUrl configured above. It sets the base for
relative assets and hyperlinks: with https://your-site.example/portal/,
css/document.css resolves to https://your-site.example/portal/css/document.css.
Paths starting with / still resolve from the site's root. Keep the trailing slash and
place <base> before stylesheet links; do not add a second one to an existing template.
Use reachable URLs for stylesheets, images, and fonts. The template does not automatically
include the Next.js page's styles or layout. If you use only inline content and absolute
asset URLs, you can omit <base> and its context value.
For local assets, you can instead package the HTML and assets in a ZIP archive.
Keep paths relative to the archive's folders and omit the website <base> for that approach.
For more conversion inputs and PDF settings, see the Node.js examples and API reference.