PDF generation from Sanity data with NextJS - tool recommendations?

1 replies
Last updated: Mar 22, 2021
Anyone have some recommendations for generating PDFs from Sanity data? All the data is also shown as HTML, so I guess I could generate the PDFs from the HTML or straight from Sanity, but not sure which solutions would be best and which tools best suitedEdit: I'm using NextJS
AI Update

There are several solid approaches for generating PDFs from Sanity data in Next.js, and I'll walk you through the most popular options the community uses.

The Main Approaches

1. HTML-to-PDF with Puppeteer (Most Common)

Since you already have HTML rendering, using Puppeteer or Playwright to convert HTML to PDF is probably your best bet. For Next.js on Vercel specifically, you'll want to use:

  • puppeteer-core (instead of full Puppeteer)
  • @sparticuz/chromium or @sparticuz/chromium-min (provides the Chrome binary for serverless environments)

Here's a typical setup in a Next.js App Router route handler (app/api/generate-pdf/route.ts):

import puppeteer from 'puppeteer-core';
import chromium from '@sparticuz/chromium';

export async function POST(request: Request) {
  const { slug } = await request.json();
  
  const browser = await puppeteer.launch({
    args: chromium.args,
    defaultViewport: chromium.defaultViewport,
    executablePath: await chromium.executablePath(),
    headless: chromium.headless,
  });
  
  const page = await browser.newPage();
  await page.goto(`${process.env.NEXT_PUBLIC_URL}/your-content/${slug}`, {
    waitUntil: 'networkidle0',
  });
  
  const pdf = await page.pdf({
    format: 'A4',
    printBackground: true,
  });
  
  await browser.close();
  
  return new Response(pdf, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="${slug}.pdf"`,
    },
  });
}

Check out the @sparticuz/chromium documentation for Vercel-specific configuration details and the Vercel docs on using Puppeteer.

2. React-PDF or PDFKit (Direct Generation)

If you want more control over PDF layout or don't want the overhead of running a headless browser, consider:

With React-PDF, you'd create PDF-specific components and render your Portable Text or Sanity data directly:

import { Document, Page, Text, View } from '@react-pdf/renderer';
import { pdf } from '@react-pdf/renderer';

const MyDocument = ({ sanityData }) => (
  <Document>
    <Page>
      <View>
        <Text>{sanityData.title}</Text>
        {/* Map your Sanity content here */}
      </View>
    </Page>
  </Document>
);

// In your route handler
const pdfBlob = await pdf(<MyDocument sanityData={data} />).toBlob();

3. Third-Party Services

Services like DocRaptor, PDFShift, or Gotenberg can handle the heavy lifting if you don't want to manage Chrome binaries in serverless environments.

Which Approach to Choose?

Go with Puppeteer if:

  • You already have well-styled HTML pages
  • You want pixel-perfect PDFs matching your web design
  • You're okay with the cold-start overhead and larger function sizes

Go with React-PDF if:

  • You need precise control over PDF layout
  • You want smaller serverless functions
  • Your PDF design differs significantly from your web design
  • You're generating lots of PDFs and want better performance

Go with a service if:

  • You want to avoid managing Chrome binaries
  • You need advanced PDF features (forms, annotations, etc.)
  • Budget allows for per-PDF costs

Handling Portable Text

If your Sanity data includes Portable Text, you'll need to serialize it. With Puppeteer, you can use @portabletext/react on your HTML page. With React-PDF, you'll need to write a custom serializer since React-PDF uses its own component system.

Sanity Functions Alternative

Worth mentioning: you could also use Sanity Functions to generate PDFs as part of your content workflow (e.g., auto-generate a PDF when a document is published). This keeps the compute workload inside Sanity's infrastructure and can be triggered by content changes. However, for on-demand user-initiated PDF generation in your Next.js app, a route handler is more straightforward.

The Puppeteer + @sparticuz/chromium approach on Vercel is probably your quickest path to success since you already have HTML rendering set up!

Show original thread
1 reply
in my experience any other than browser based pdf generators for websites make a mess of your layout. So I recommend making the page with next (makes debugging also easier) and using a serverless function that sets up a headless browser, visits the page and generates the download.
I've used both puppeteer (
https://github.com/puppeteer/puppeteer ) and playwright (https://github.com/microsoft/playwright ) successfully for generating very nice pdfs - with clickable links and everything.
Here's a super basic one I did on aws. Just shoot it a url and filename and it downloads that page as pdf. (This will eventually need some form of authentication)


const playwright = require("playwright-aws-lambda");

const CUSTOM_CSS = `
  // whatever custom css for print/pdf
`;

exports.lambdaHandler = async (event, context) => {
  if (!event.queryStringParameters) {
    return {
      statusCode: 500,
      body: JSON.stringify("missing parameters"),
    };
  }

  if (!event.queryStringParameters || !event.queryStringParameters.url) {
    return {
      statusCode: 500,
      body: JSON.stringify("missing parameter url"),
    };
  }

  if (!event.queryStringParameters.filename) {
    return {
      statusCode: 500,
      body: JSON.stringify("missing parameter filename"),
    };
  }

  try {
    const browser = await playwright.launchChromium();
    const context = await browser.newContext();
    const page = await context.newPage();
    const response = await page.goto(event.queryStringParameters.url);
    const status = await response.status();

    await page.addStyleTag({ content: CUSTOM_CSS });

    if (status !== 200) {
      return {
        statusCode: 500,
        body: JSON.stringify("page not found"),
      };
    }

    await page.emulateMedia({ media: "screen", format: "A4" });
    const file = await page.pdf({
      printBackground: true,
    });
    await browser.close();

    return {
      statusCode: 200,
      body: file.toString("base64"),
      isBase64Encoded: true,
      headers: {
        "Content-Type": "application/pdf",
        "Content-Disposition": `attachment; filename=${event.queryStringParameters.filename}`,
      },
    };
  } catch (err) {
    console.log(err);
    return err;
  }
};

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?