Guide

How to Convert Markdown to PDF in Next.js

Updated July 15, 2026

Generating documents dynamically from Markdown is a core feature for modern Next.js applications, especially when building invoice systems, contract managers, or static documentation portals. Depending on your scaling model, you can choose between lightweight client-side printing or robust server-side PDF generation. Here is how to convert markdown to pdf nextjs platforms support efficiently.

To handle this, we will review the client-side approach and the serverless API route approach using Puppeteer.

Option 1: Client-Side Print Trigger (Fast & Serverless)

If you just need a button that says “Save as PDF” on a dashboard page, doing it on the client side using the standard browser print engine is the simplest route. This approach is highly efficient for private tools or client-side utilities.

First, render your Markdown content in Next.js using marked or a standard Markdown renderer:

'use client';

import React from 'react';
import { marked } from 'marked';

interface PrinterProps {
  markdown: string;
}

export default function ClientPrinter({ markdown }: PrinterProps) {
  const htmlContent = marked.parse(markdown);

  const triggerPrint = () => {
    window.print();
  };

  return (
    <div class="p-6">
      <button 
        onClick={triggerPrint} 
        className="no-print mb-4 px-4 py-2 bg-black text-white rounded"
      >
        Export PDF
      </button>
      <div 
        className="print-content"
        dangerouslySetInnerHTML={{ __html: htmlContent }} 
      />
    </div>
  );
}

CSS Styling Rules for Print

To ensure the navigation headers, sidebars, and buttons don’t show up in the printed PDF, configure your CSS file:

@media print {
  /* Hide UI elements */
  .no-print {
    display: none !important;
  }

  /* Style printed container */
  .print-content {
    width: 100%;
    margin: 0;
    padding: 20mm;
    font-size: 14px;
    line-height: 1.6;
  }
}

This client-side route costs nothing to run, handles draft storage easily, and keeps user content private. However, user device printing rules vary, leading to slight page layout inconsistencies.

Option 2: Serverless API Route with Puppeteer (Consistent)

If your app needs to email a PDF invoice to a client or archive reports automatically, you cannot rely on the client’s browser. You must generate the PDF on the server. In Next.js, this is done using an API Route (Route Handler).

Since Vercel serverless functions have size limits (50MB), running standard Puppeteer can be tricky because it bundles a full Chromium browser. We must use a lightweight wrapper like @sparticuz/chromium designed specifically for serverless runtimes.

Step 1: Install Serverless Dependencies

Install the marked compiler, puppeteer-core, and sparticuz chromium:

npm install marked puppeteer-core @sparticuz/chromium

Step 2: Create the Next.js Route Handler

Create a route file at /app/api/pdf/route.ts:

import { NextResponse } from 'next/server';
import puppeteer from 'puppeteer-core';
import chromium from '@sparticuz/chromium';
import { marked } from 'marked';

export async function POST(request: Request) {
  try {
    const { markdown } = await request.json();
    if (!markdown) {
      return NextResponse.json({ error: 'Missing markdown content' }, { status: 400 });
    }

    const htmlBody = marked.parse(markdown);
    const htmlTemplate = `
      <!DOCTYPE html>
      <html>
        <head>
          <style>
            body { font-family: system-ui; padding: 30px; }
            h1 { color: #111; }
            table { width: 100%; border-collapse: collapse; margin-top: 15px; }
            th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
          </style>
        </head>
        <body>${htmlBody}</body>
      </html>
    `;

    // Configure headless browser options for serverless runtimes
    const browser = await puppeteer.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath(),
      headless: chromium.headless === 'true' ? true : chromium.headless,
    });

    const page = await browser.newPage();
    await page.setContent(htmlTemplate, { waitUntil: 'networkidle0' });
    
    const pdf = await page.pdf({
      format: 'A4',
      printBackground: true,
      margin: { top: '15mm', bottom: '15mm', left: '15mm', right: '15mm' }
    });

    await browser.close();

    // Return the raw PDF bytes
    return new Response(pdf, {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename="document.pdf"',
      },
    });
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

This API route accepts raw markdown, parses it, compiles it inside headless Chromium, and streams a binary PDF directly back to your React frontend.

Bottom Line

If you are developing public utility pages, simple resume converters, or documentation sites, client-side PDF export triggered by a React component is the fastest, zero-cost choice. If your product demands automatic delivery (e.g., invoices sent on purchase), build a Next.js serverless route handler using Puppeteer and Sparticuz.

To test how your Markdown files format into PDF layouts without writing code, use our free online editor or test our markdown report generator for instant print previews and adjustments.

Written by Markdown to PDF Editorial Team

Our team specializes in document design, web standards, and developer utilities. This guide was researched and vetted against current browser printing standards and Paged.js specifications. Learn more on our About page.

Try it yourself — free, no signup

Convert your Markdown to a polished PDF right in your browser.

Open the editor