npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@maistik/nuxt-pdf

v1.1.5

Published

A Nuxt 3 & 4 module for server-side PDF generation using Handlebars templates

Readme

@maistik/nuxt-pdf

npm version npm downloads License: MIT Nuxt

A powerful Nuxt 3 & 4 module for server-side PDF generation using Handlebars templates. Generate beautiful, data-driven PDFs with support for multiple rendering providers (Gotenberg, Browserless, Puppeteer) and built-in internationalization.

Everything runs server-side in Nitro/Node — no PDF code is shipped to the browser.


Table of Contents


Features

  • 🎨 Handlebars templates — lightweight templating, no Vue/SSR overhead
  • 🌍 Internationalization — built-in i18n with the {{t}} helper and nested keys
  • 🔄 Provider-agnostic — Gotenberg, Browserless, or Puppeteer, switchable via config
  • 🎯 SSR-safe — rendering happens entirely server-side in Nitro/Node
  • 🧩 Composable API — simple usePdf() for generate() and download()
  • 🧮 Rich helper library — currency, dates, numbers, math, string and comparison helpers
  • 🛠️ Custom helpers — define your own helpers in nuxt.config (works in production)
  • 📐 Print-aware@page, page breaks, and @media print styling
  • 🎪 Playground — demo app with Invoice & Sales Report templates in 3 languages

Requirements

| Requirement | Version | | ----------- | ------- | | Nuxt | 3.x or 4.x | | Node.js | 18, 20, or 22 (CI-tested; 22 recommended) | | Handlebars | ^4.7 (peer-installed alongside the module) |

A Chromium/Chrome runtime is required only for the puppeteer provider. The gotenberg and browserless providers run the browser remotely.


Installation

# pnpm
pnpm add @maistik/nuxt-pdf handlebars

# npm
npm install @maistik/nuxt-pdf handlebars

# yarn
yarn add @maistik/nuxt-pdf handlebars

handlebars is a peer dependency so your app and the module share a single instance. puppeteer ships as a dependency and is only loaded at runtime when the puppeteer provider is selected.


Quick Start

1. Register the module

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@maistik/nuxt-pdf'],
  pdf: {
    provider: 'puppeteer', // 'gotenberg' | 'browserless' | 'puppeteer'
    components: ['pdf'], // where your .hbs templates live
    sharedComponents: ['pdf/partials'], // where your .hbs partials live
    enableI18n: true,
    defaultLocale: 'en',
    availableLocales: ['en', 'es'],
    i18nMessages: {
      en: { invoice: { title: 'Invoice', total: 'Total' } },
      es: { invoice: { title: 'Factura', total: 'Total' } },
    },
  },
})

2. Create a template

Create pdf/Invoice.hbs in your project root:

<style>
  @page { size: A4; margin: 20mm; }
  body { font-family: Arial, sans-serif; color: #333; }
  .header { text-align: center; margin-bottom: 30px; }
  .total { font-weight: bold; font-size: 18px; }
</style>

<div class="header">
  <h1>{{t "invoice.title"}}</h1>
  <p>Invoice #{{invoiceNumber}}</p>
</div>

<table>
  {{#each items}}
  <tr>
    <td>{{this.description}}</td>
    <td>{{formatCurrency this.price}}</td>
  </tr>
  {{/each}}
</table>

<div class="total">
  {{t "invoice.total"}}: {{formatCurrency total}}
</div>

3. Generate the PDF

<script setup lang="ts">
const { generate, download } = usePdf()

async function makeInvoice() {
  const data = {
    invoiceNumber: 'INV-001',
    items: [{ description: 'Service', price: 100 }],
    total: 100,
  }

  // Get a Blob (preview, upload, attach to email, ...)
  const blob = await generate('Invoice', data, { format: 'A4' }, 'en')

  // Or trigger a browser download directly
  await download('Invoice', data, { format: 'A4' }, 'invoice.pdf', 'en')
}
</script>

Project Structure

Template directories are resolved relative to your project root (not srcDir), so the same layout works for both Nuxt 3 and Nuxt 4 (which moves srcDir to app/):

your-project/
├─ nuxt.config.ts
├─ pdf/                      # `components` → templates
│  ├─ Invoice.hbs           # referenced as "Invoice"
│  ├─ SalesReport.hbs       # referenced as "SalesReport"
│  └─ partials/             # `sharedComponents` → partials
│     ├─ header.hbs         # used as {{> header}}
│     └─ footer.hbs         # used as {{> footer}}
└─ ...
  • Templates are discovered recursively by their .hbs extension.
  • A template's name is its filename without extension (Invoice.hbs"Invoice").
  • Partials are registered under their filename and used with {{> name}}.

Configuration Reference

All options live under the pdf key in nuxt.config.ts.

| Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | provider | 'gotenberg' \| 'browserless' \| 'puppeteer' | 'gotenberg' | Active rendering backend. | | components | string[] | ['pdf'] | Directories (relative to project root) scanned for .hbs templates. | | sharedComponents | string[] | ['pdf/partials'] | Directories scanned for .hbs partials. | | enableI18n | boolean | true | Enables the {{t}} helper and locale message lookup. | | defaultLocale | string | 'en' | Locale used when none is passed to generate(). | | availableLocales | string[] | ['en'] | Locales exposed to the client via getAvailableLocales(). | | i18nMessages | Record<string, Record<string, unknown>> | {} | Nested translation messages keyed by locale. | | providers | object | see below | Per-provider connection settings. | | defaultOptions | PdfOptions | see below | Default render options merged with per-call options. | | customHelpers | Record<string, Function> | {} | Extra Handlebars helpers (see Custom Helpers). |

providers: {
  gotenberg: { url: 'http://localhost:3000' },
  browserless: { url: 'https://chrome.browserless.io', apiKey: '' },
  puppeteer: {
    launchOptions: {
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox'],
    },
  },
},
defaultOptions: {
  format: 'A4',
  margin: { top: 20, bottom: 20, left: 20, right: 20 }, // millimetres
  landscape: false,
  printBackground: true,
  pageBreak: {
    before: ['always'],
    after: ['always'],
    avoid: ['.no-break'],
  },
},

Margins are expressed in millimetres. The Gotenberg provider converts them to inches automatically.


Providers

Switch backends by changing provider and supplying the matching providers entry.

Puppeteer (local / on-prem)

Best for development and single-server deployments. Uses the Chromium bundled with Puppeteer by default; point it at a system Chrome with executablePath.

pdf: {
  provider: 'puppeteer',
  providers: {
    puppeteer: {
      launchOptions: {
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox'],
        // Optional — defaults to Puppeteer's bundled Chromium:
        // executablePath: process.env.CHROME_PATH,
      },
    },
  },
}

launchOptions is passed straight to puppeteer.launch(). --no-sandbox is commonly required inside containers, but only disable the sandbox in trusted environments.

Gotenberg (Docker)

Ideal for containerized stacks. Run Gotenberg alongside your app:

docker run --rm -p 3000:3000 gotenberg/gotenberg:8
pdf: {
  provider: 'gotenberg',
  providers: {
    gotenberg: { url: 'http://gotenberg:3000' },
  },
}

Browserless (cloud / self-hosted)

Great for serverless deployments where you don't manage a browser:

pdf: {
  provider: 'browserless',
  providers: {
    browserless: {
      url: 'https://chrome.browserless.io',
      apiKey: process.env.BROWSERLESS_API_KEY,
    },
  },
}

| Provider | Where Chrome runs | Best for | | -------- | ----------------- | -------- | | puppeteer | In-process, local | Dev, on-prem, full control | | gotenberg | Separate container | Docker / Kubernetes | | browserless | Remote service | Serverless / managed |


Internationalization (i18n)

Provide nested messages per locale and look them up with the {{t}} helper using dot-separated keys:

i18nMessages: {
  en: { invoice: { title: 'Invoice', total: 'Total' } },
  fr: { invoice: { title: 'Facture', total: 'Total' } },
}
<h1>{{t "invoice.title"}}</h1>
  • The locale is chosen per call (generate(..., locale)), falling back to defaultLocale.
  • Missing keys render the key itself (e.g. invoice.unknown) so gaps are visible.
  • The same locale drives Intl-based helpers (formatCurrency, formatDate, formatNumber).

Built-in Helpers

These helpers are always registered, regardless of configuration.

Internationalization

{{t "invoice.title"}}              <!-- localized text (or the key if missing) -->

Currency

{{formatCurrency 1234.56}}         <!-- $1,234.56 (USD by default) -->
{{formatCurrency 1234.56 "EUR"}}   <!-- €1,234.56 -->
{{formatCurrency amount currency="GBP"}}

Dates

{{formatDate date}}                <!-- short style, e.g. 1/15/24 -->
{{formatDate date "full"}}         <!-- full style, e.g. Monday, January 15, 2024 -->
{{formatDate date format="long"}}  <!-- styles: full | long | medium | short -->

Numbers

{{formatNumber 1234.56}}                 <!-- 1,234.56 -->
{{formatNumber 0.15 style="percent"}}    <!-- 15% -->
{{formatNumber n minimumFractionDigits=2}}

formatNumber forwards any hash arguments to Intl.NumberFormat.

Math

{{add 10 5}}            <!-- 15 -->
{{subtract 10 5}}       <!-- 5 -->
{{multiply 10 5}}       <!-- 50 -->
{{divide 10 5}}         <!-- 2  (returns 0 when dividing by 0) -->
{{percentage 25 100}}   <!-- 25.00% -->
{{lineTotal qty price}} <!-- qty * price, fixed to 2 decimals -->

Strings

{{upper "hello world"}}          <!-- HELLO WORLD -->
{{lower "HELLO WORLD"}}          <!-- hello world -->
{{capitalize "hello world"}}     <!-- Hello world -->
{{truncate "Long text here" 10}} <!-- Long text ... -->

Comparison

Work both inline (returning a boolean) and as block helpers:

{{#if (eq status "paid")}}Paid{{/if}}
{{#eq a b}}equal{{else}}different{{/eq}}
{{#ne a b}}not equal{{/ne}}
{{#gt total 1000}}High value{{/gt}}

Custom Helpers

Define your own Handlebars helpers in nuxt.config.ts. They are compiled into a server-side module at build time, so they work in development and production:

pdf: {
  customHelpers: {
    // Simple value transformation
    shout: (value: string) => `${String(value ?? '').toUpperCase()}!`,

    // Repeat a string
    repeat: (str: string, times: number) => String(str).repeat(times || 1),

    // Block helper with conditional logic
    ifEquals(this: unknown, a: unknown, b: unknown, options: any) {
      return a === b ? options.fn(this) : options.inverse(this)
    },
  },
}
{{shout "hello"}}                 <!-- HELLO! -->
{{repeat "★" 5}}                  <!-- ★★★★★ -->
{{#ifEquals status "active"}}Active{{else}}Inactive{{/ifEquals}}

⚠️ Custom helpers must be self-contained. Only each function's source is serialized into the runtime module — a helper cannot reference variables, imports, or values from the surrounding nuxt.config.ts scope. Keep all logic inside the function body.


Partials

Create reusable fragments in any sharedComponents directory:

<!-- pdf/partials/header.hbs -->
<div class="header">
  <h1>{{title}}</h1>
  <p>{{subtitle}}</p>
</div>

Use them in templates, passing parameters as needed:

{{> header title="My Document" subtitle="Generated Report"}}

Automatic Data Enrichment

For convenience, the module derives some fields based on the template name:

Invoice templates (name contains invoice) receive, when items[] is present:

  • subtotal — sum of quantity × price across items
  • taxsubtotal × (taxRate ?? 0.1)
  • totalsubtotal + tax
  • dueDateissueDate + paymentTerms days (when both are provided)

Sales report templates (name contains salesreport) receive, when salesData[] is present:

  • quarters — quarterly totals derived from each entry's date/amount
  • rating — performance rating from total sales (Excellent / Good / Average / Needs Improvement)

Provide these values yourself if you don't want them computed — your data is spread first, then enriched.


Composable API — usePdf()

const {
  generate,            // (template, data, options?, locale?) => Promise<Blob>
  download,            // (template, data, options?, filename?, locale?) => Promise<void>
  getAvailableLocales, // () => string[]
  getDefaultLocale,    // () => string
} = usePdf()

| Method | Signature | Notes | | ------ | --------- | ----- | | generate | (template, data, options?, locale?) => Promise<Blob> | Returns an application/pdf Blob. | | download | (template, data, options?, filename?, locale?) => Promise<void> | Browser-only — creates and clicks a download link. | | getAvailableLocales | () => string[] | Reads availableLocales from public runtime config. | | getDefaultLocale | () => string | Reads defaultLocale from public runtime config. |

When no locale is passed, defaultLocale is used.


Server API — POST /api/pdf

The composable calls a server route that you can also hit directly (e.g. from other services). The module registers it automatically.

Request

POST /api/pdf
Content-Type: application/json
{
  "template": "Invoice",
  "ctx": {
    "data": { "invoiceNumber": "INV-001", "items": [] },
    "options": { "format": "A4" },
    "locale": "en"
  }
}

Response — binary application/pdf body.

Status codes

| Code | Meaning | | ---- | ------- | | 200 | PDF generated; body is the PDF bytes. | | 400 | Missing/invalid template or ctx. | | 404 | template does not match any discovered .hbs file. | | 500 | Rendering or provider failure (details are logged server-side, not leaked). |


PDF Options

Passed as the options argument to generate()/download() and merged over defaultOptions:

interface PdfOptions {
  format?: 'A4' | 'Letter' | 'Legal'
  margin?: {
    top?: number    // millimetres
    bottom?: number
    left?: number
    right?: number
  }
  landscape?: boolean
  printBackground?: boolean
  pageBreak?: {
    before?: string[]
    after?: string[]
    avoid?: string[]
  }
}

Print-aware CSS

Templates are plain HTML/CSS, so you can use print rules directly:

@page { size: A4; margin: 20mm; }

.page-break { page-break-before: always; }

@media print {
  .no-break { page-break-inside: avoid; }
}

Development

This repository uses pnpm.

pnpm install          # install dependencies
pnpm dev              # run the playground with HMR
pnpm dev:build        # build the playground (Nitro)
pnpm build            # build the module
pnpm lint             # eslint
pnpm test:unit        # vitest unit tests
pnpm test:e2e         # playwright end-to-end tests
pnpm test             # unit + e2e

The playground/ app demonstrates Invoice and Sales Report templates in English, Spanish, and French, plus a custom helper and provider configuration examples.


Troubleshooting

Template "X" not found (404). The template must match a .hbs filename (without extension) inside one of your components directories. Remember the lookup is by filename, not path.

Puppeteer can't find Chrome. Either rely on Puppeteer's bundled Chromium (omit executablePath) or set CHROME_PATH/executablePath to a valid Chrome binary. In containers you'll usually need args: ['--no-sandbox'].

Gotenberg/Browserless errors. Confirm the url is reachable from the server and, for Browserless, that apiKey is set.

My custom helper does nothing. Ensure it's a function and fully self-contained — it cannot close over values defined elsewhere in nuxt.config.ts (only the function source is serialized).


License

MIT


Support