@maistik/nuxt-pdf 1.0.17 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,73 +1,120 @@
1
+ <p align="center">
2
+ <img src="cover.png" alt="@maistik/nuxt-pdf" width="100%" />
3
+ </p>
4
+
1
5
  # @maistik/nuxt-pdf
2
6
 
3
- A powerful Nuxt 3/4 module for server-side PDF generation using Handlebars templates. Generate beautiful, data-driven PDFs with support for multiple providers (Gotenberg, Browserless, Puppeteer Core) and built-in internationalization.
7
+ [![npm version](https://img.shields.io/npm/v/@maistik/nuxt-pdf.svg)](https://www.npmjs.com/package/@maistik/nuxt-pdf)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@maistik/nuxt-pdf.svg)](https://www.npmjs.com/package/@maistik/nuxt-pdf)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
10
+ [![Nuxt](https://img.shields.io/badge/Nuxt-3%20%26%204-00DC82?logo=nuxt.js)](https://nuxt.com)
11
+
12
+ A powerful Nuxt 3 & 4 module for **server-side PDF generation** using [Handlebars](https://handlebarsjs.com/) templates. Generate beautiful, data-driven PDFs with support for multiple rendering providers (Gotenberg, Browserless, Puppeteer) and built-in internationalization.
13
+
14
+ Everything runs server-side in Nitro/Node — no PDF code is shipped to the browser.
15
+
16
+ ---
17
+
18
+ ## Table of Contents
19
+
20
+ - [Features](#features)
21
+ - [Requirements](#requirements)
22
+ - [Installation](#installation)
23
+ - [Quick Start](#quick-start)
24
+ - [Project Structure](#project-structure)
25
+ - [Configuration Reference](#configuration-reference)
26
+ - [Providers](#providers)
27
+ - [Internationalization (i18n)](#internationalization-i18n)
28
+ - [Built-in Helpers](#built-in-helpers)
29
+ - [Custom Helpers](#custom-helpers)
30
+ - [Partials](#partials)
31
+ - [Automatic Data Enrichment](#automatic-data-enrichment)
32
+ - [Composable API — `usePdf()`](#composable-api--usepdf)
33
+ - [Server API — `POST /api/pdf`](#server-api--post-apipdf)
34
+ - [PDF Options](#pdf-options)
35
+ - [Development](#development)
36
+ - [Troubleshooting](#troubleshooting)
37
+ - [License](#license)
38
+
39
+ ---
4
40
 
5
41
  ## Features
6
42
 
7
- - 🎨 **Handlebars Templates** - Lightweight templating without Vue overhead
8
- - 🌍 **Internationalization** - Built-in i18n support with `{{t}}` helper
9
- - 🔄 **Provider Agnostic** - Support for Gotenberg, Browserless, and Puppeteer Core
10
- - 🎯 **SSR Safe** - Everything runs server-side in Nitro/Node
11
- - 🧩 **Composable API** - Easy-to-use `usePdf()` composable
12
- - 📱 **Responsive Design** - CSS-based layouts with print media queries
13
- - 🎪 **Playground** - Demo app with sample templates
43
+ - 🎨 **Handlebars templates** lightweight templating, no Vue/SSR overhead
44
+ - 🌍 **Internationalization** built-in i18n with the `{{t}}` helper and nested keys
45
+ - 🔄 **Provider-agnostic** Gotenberg, Browserless, or Puppeteer, switchable via config
46
+ - 🎯 **SSR-safe** rendering happens entirely server-side in Nitro/Node
47
+ - 🧩 **Composable API** simple `usePdf()` for `generate()` and `download()`
48
+ - 🧮 **Rich helper library** currency, dates, numbers, math, string and comparison helpers
49
+ - 🛠️ **Custom helpers** define your own helpers in `nuxt.config` (works in production)
50
+ - 📐 **Print-aware** — `@page`, page breaks, and `@media print` styling
51
+ - 🎪 **Playground** — demo app with Invoice & Sales Report templates in 3 languages
14
52
 
15
- ## Quick Start
53
+ ---
16
54
 
17
- ### Installation
55
+ ## Requirements
56
+
57
+ | Requirement | Version |
58
+ | ----------- | ------- |
59
+ | Nuxt | `3.x` or `4.x` |
60
+ | Node.js | `18`, `20`, or `22` (CI-tested; `22` recommended) |
61
+ | Handlebars | `^4.7` (peer-installed alongside the module) |
62
+
63
+ A Chromium/Chrome runtime is required **only** for the `puppeteer` provider. The
64
+ `gotenberg` and `browserless` providers run the browser remotely.
65
+
66
+ ---
67
+
68
+ ## Installation
18
69
 
19
70
  ```bash
71
+ # pnpm
72
+ pnpm add @maistik/nuxt-pdf handlebars
73
+
74
+ # npm
20
75
  npm install @maistik/nuxt-pdf handlebars
76
+
77
+ # yarn
78
+ yarn add @maistik/nuxt-pdf handlebars
21
79
  ```
22
80
 
23
- ### Configuration
81
+ > `handlebars` is a peer dependency so your app and the module share a single
82
+ > instance. `puppeteer` ships as a dependency and is only loaded at runtime when
83
+ > the `puppeteer` provider is selected.
84
+
85
+ ---
86
+
87
+ ## Quick Start
24
88
 
25
- Add the module to your `nuxt.config.ts`:
89
+ ### 1. Register the module
26
90
 
27
- ```typescript
91
+ ```ts
92
+ // nuxt.config.ts
28
93
  export default defineNuxtConfig({
29
94
  modules: ['@maistik/nuxt-pdf'],
30
95
  pdf: {
31
- provider: 'puppeteer', // or 'gotenberg' or 'browserless'
32
- components: ['pdf'],
33
- sharedComponents: ['pdf/partials'],
96
+ provider: 'puppeteer', // 'gotenberg' | 'browserless' | 'puppeteer'
97
+ components: ['pdf'], // where your .hbs templates live
98
+ sharedComponents: ['pdf/partials'], // where your .hbs partials live
34
99
  enableI18n: true,
35
100
  defaultLocale: 'en',
36
- availableLocales: ['en', 'es', 'fr'],
101
+ availableLocales: ['en', 'es'],
37
102
  i18nMessages: {
38
- en: {
39
- invoice: {
40
- title: 'Invoice',
41
- total: 'Total'
42
- }
43
- },
44
- es: {
45
- invoice: {
46
- title: 'Factura',
47
- total: 'Total'
48
- }
49
- }
103
+ en: { invoice: { title: 'Invoice', total: 'Total' } },
104
+ es: { invoice: { title: 'Factura', total: 'Total' } },
50
105
  },
51
- providers: {
52
- puppeteer: {
53
- launchOptions: {
54
- headless: true,
55
- args: ['--no-sandbox']
56
- }
57
- }
58
- }
59
- }
106
+ },
60
107
  })
61
108
  ```
62
109
 
63
- ### Create a Template
110
+ ### 2. Create a template
64
111
 
65
- Create `pdf/Invoice.hbs` in your project:
112
+ Create `pdf/Invoice.hbs` in your project root:
66
113
 
67
114
  ```handlebars
68
115
  <style>
69
116
  @page { size: A4; margin: 20mm; }
70
- body { font-family: Arial, sans-serif; }
117
+ body { font-family: Arial, sans-serif; color: #333; }
71
118
  .header { text-align: center; margin-bottom: 30px; }
72
119
  .total { font-weight: bold; font-size: 18px; }
73
120
  </style>
@@ -91,198 +138,300 @@ Create `pdf/Invoice.hbs` in your project:
91
138
  </div>
92
139
  ```
93
140
 
94
- ### Generate PDFs
95
-
96
- Use the composable in your Vue components:
141
+ ### 3. Generate the PDF
97
142
 
98
143
  ```vue
99
- <script setup>
144
+ <script setup lang="ts">
100
145
  const { generate, download } = usePdf()
101
146
 
102
- const generateInvoice = async () => {
147
+ async function makeInvoice() {
103
148
  const data = {
104
149
  invoiceNumber: 'INV-001',
105
- items: [
106
- { description: 'Service', price: 100 }
107
- ],
108
- total: 100
150
+ items: [{ description: 'Service', price: 100 }],
151
+ total: 100,
109
152
  }
110
-
111
- // Generate and preview
153
+
154
+ // Get a Blob (preview, upload, attach to email, ...)
112
155
  const blob = await generate('Invoice', data, { format: 'A4' }, 'en')
113
-
114
- // Or download directly
156
+
157
+ // Or trigger a browser download directly
115
158
  await download('Invoice', data, { format: 'A4' }, 'invoice.pdf', 'en')
116
159
  }
117
160
  </script>
118
161
  ```
119
162
 
163
+ ---
164
+
165
+ ## Project Structure
166
+
167
+ Template directories are resolved relative to your **project root** (not `srcDir`),
168
+ so the same layout works for both Nuxt 3 and Nuxt 4 (which moves `srcDir` to `app/`):
169
+
170
+ ```
171
+ your-project/
172
+ ├─ nuxt.config.ts
173
+ ├─ pdf/ # `components` → templates
174
+ │ ├─ Invoice.hbs # referenced as "Invoice"
175
+ │ ├─ SalesReport.hbs # referenced as "SalesReport"
176
+ │ └─ partials/ # `sharedComponents` → partials
177
+ │ ├─ header.hbs # used as {{> header}}
178
+ │ └─ footer.hbs # used as {{> footer}}
179
+ └─ ...
180
+ ```
181
+
182
+ - Templates are discovered **recursively** by their `.hbs` extension.
183
+ - A template's **name is its filename without extension** (`Invoice.hbs` → `"Invoice"`).
184
+ - Partials are registered under their filename and used with `{{> name}}`.
185
+
186
+ ---
187
+
188
+ ## Configuration Reference
189
+
190
+ All options live under the `pdf` key in `nuxt.config.ts`.
191
+
192
+ | Option | Type | Default | Description |
193
+ | ------ | ---- | ------- | ----------- |
194
+ | `provider` | `'gotenberg' \| 'browserless' \| 'puppeteer'` | `'gotenberg'` | Active rendering backend. |
195
+ | `components` | `string[]` | `['pdf']` | Directories (relative to project root) scanned for `.hbs` templates. |
196
+ | `sharedComponents` | `string[]` | `['pdf/partials']` | Directories scanned for `.hbs` partials. |
197
+ | `enableI18n` | `boolean` | `true` | Enables the `{{t}}` helper and locale message lookup. |
198
+ | `defaultLocale` | `string` | `'en'` | Locale used when none is passed to `generate()`. |
199
+ | `availableLocales` | `string[]` | `['en']` | Locales exposed to the client via `getAvailableLocales()`. |
200
+ | `i18nMessages` | `Record<string, Record<string, unknown>>` | `{}` | Nested translation messages keyed by locale. |
201
+ | `providers` | `object` | see below | Per-provider connection settings. |
202
+ | `defaultOptions` | `PdfOptions` | see below | Default render options merged with per-call options. |
203
+ | `customHelpers` | `Record<string, Function>` | `{}` | Extra Handlebars helpers (see [Custom Helpers](#custom-helpers)). |
204
+
205
+ <details>
206
+ <summary><strong>Default <code>providers</code> and <code>defaultOptions</code></strong></summary>
207
+
208
+ ```ts
209
+ providers: {
210
+ gotenberg: { url: 'http://localhost:3000' },
211
+ browserless: { url: 'https://chrome.browserless.io', apiKey: '' },
212
+ puppeteer: {
213
+ launchOptions: {
214
+ headless: true,
215
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
216
+ },
217
+ },
218
+ },
219
+ defaultOptions: {
220
+ format: 'A4',
221
+ margin: { top: 20, bottom: 20, left: 20, right: 20 }, // millimetres
222
+ landscape: false,
223
+ printBackground: true,
224
+ pageBreak: {
225
+ before: ['always'],
226
+ after: ['always'],
227
+ avoid: ['.no-break'],
228
+ },
229
+ },
230
+ ```
231
+
232
+ > **Margins are expressed in millimetres.** The Gotenberg provider converts them
233
+ > to inches automatically.
234
+
235
+ </details>
236
+
237
+ ---
238
+
120
239
  ## Providers
121
240
 
122
- ### Puppeteer Core (Local)
241
+ Switch backends by changing `provider` and supplying the matching `providers` entry.
123
242
 
124
- Best for development and on-premise deployments:
243
+ ### Puppeteer (local / on-prem)
125
244
 
126
- ```typescript
245
+ Best for development and single-server deployments. Uses the Chromium bundled with
246
+ Puppeteer by default; point it at a system Chrome with `executablePath`.
247
+
248
+ ```ts
127
249
  pdf: {
128
250
  provider: 'puppeteer',
129
251
  providers: {
130
252
  puppeteer: {
131
253
  launchOptions: {
132
254
  headless: true,
133
- args: ['--no-sandbox', '--disable-setuid-sandbox']
134
- }
135
- }
136
- }
255
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
256
+ // Optional — defaults to Puppeteer's bundled Chromium:
257
+ // executablePath: process.env.CHROME_PATH,
258
+ },
259
+ },
260
+ },
137
261
  }
138
262
  ```
139
263
 
264
+ > `launchOptions` is passed straight to `puppeteer.launch()`. `--no-sandbox` is
265
+ > commonly required inside containers, but only disable the sandbox in trusted
266
+ > environments.
267
+
140
268
  ### Gotenberg (Docker)
141
269
 
142
- Perfect for containerized environments:
270
+ Ideal for containerized stacks. Run Gotenberg alongside your app:
271
+
272
+ ```bash
273
+ docker run --rm -p 3000:3000 gotenberg/gotenberg:8
274
+ ```
143
275
 
144
- ```typescript
276
+ ```ts
145
277
  pdf: {
146
278
  provider: 'gotenberg',
147
279
  providers: {
148
- gotenberg: {
149
- url: 'http://gotenberg:3000'
150
- }
151
- }
280
+ gotenberg: { url: 'http://gotenberg:3000' },
281
+ },
152
282
  }
153
283
  ```
154
284
 
155
- ### Browserless (Cloud)
285
+ ### Browserless (cloud / self-hosted)
156
286
 
157
- Great for serverless deployments:
287
+ Great for serverless deployments where you don't manage a browser:
158
288
 
159
- ```typescript
289
+ ```ts
160
290
  pdf: {
161
291
  provider: 'browserless',
162
292
  providers: {
163
293
  browserless: {
164
294
  url: 'https://chrome.browserless.io',
165
- apiKey: process.env.BROWSERLESS_API_KEY
166
- }
167
- }
295
+ apiKey: process.env.BROWSERLESS_API_KEY,
296
+ },
297
+ },
168
298
  }
169
299
  ```
170
300
 
171
- ## Built-in Helpers
301
+ | Provider | Where Chrome runs | Best for |
302
+ | -------- | ----------------- | -------- |
303
+ | `puppeteer` | In-process, local | Dev, on-prem, full control |
304
+ | `gotenberg` | Separate container | Docker / Kubernetes |
305
+ | `browserless` | Remote service | Serverless / managed |
172
306
 
173
- ### Internationalization
174
- ```handlebars
175
- {{t "invoice.title"}} <!-- Outputs localized text -->
176
- ```
307
+ ---
177
308
 
178
- ### Currency Formatting
179
- ```handlebars
180
- {{formatCurrency 1234.56}} <!-- $1,234.56 -->
181
- {{formatCurrency 1234.56 "EUR"}} <!-- €1,234.56 -->
309
+ ## Internationalization (i18n)
310
+
311
+ Provide nested messages per locale and look them up with the `{{t}}` helper using
312
+ dot-separated keys:
313
+
314
+ ```ts
315
+ i18nMessages: {
316
+ en: { invoice: { title: 'Invoice', total: 'Total' } },
317
+ fr: { invoice: { title: 'Facture', total: 'Total' } },
318
+ }
182
319
  ```
183
320
 
184
- ### Date Formatting
185
321
  ```handlebars
186
- {{formatDate date}} <!-- 12/25/2024 -->
187
- {{formatDate date "full"}} <!-- Wednesday, December 25, 2024 -->
322
+ <h1>{{t "invoice.title"}}</h1>
188
323
  ```
189
324
 
190
- ### Number Formatting
325
+ - The locale is chosen per call (`generate(..., locale)`), falling back to
326
+ `defaultLocale`.
327
+ - Missing keys render the **key itself** (e.g. `invoice.unknown`) so gaps are visible.
328
+ - The same locale drives `Intl`-based helpers (`formatCurrency`, `formatDate`,
329
+ `formatNumber`).
330
+
331
+ ---
332
+
333
+ ## Built-in Helpers
334
+
335
+ These helpers are always registered, regardless of configuration.
336
+
337
+ ### Internationalization
191
338
  ```handlebars
192
- {{formatNumber 1234.56}} <!-- 1,234.56 -->
193
- {{formatNumber 0.15 style="percent"}} <!-- 15% -->
339
+ {{t "invoice.title"}} <!-- localized text (or the key if missing) -->
194
340
  ```
195
341
 
196
- ### Math Operations
342
+ ### Currency
197
343
  ```handlebars
198
- {{add 10 5}} <!-- 15 -->
199
- {{subtract 10 5}} <!-- 5 -->
200
- {{multiply 10 5}} <!-- 50 -->
201
- {{divide 10 5}} <!-- 2 -->
202
- {{percentage 25 100}} <!-- 25% -->
344
+ {{formatCurrency 1234.56}} <!-- $1,234.56 (USD by default) -->
345
+ {{formatCurrency 1234.56 "EUR"}} <!-- €1,234.56 -->
346
+ {{formatCurrency amount currency="GBP"}}
203
347
  ```
204
348
 
205
- ## Custom Helpers
206
-
207
- You can define your own custom helpers in the configuration:
208
-
209
- ```typescript
210
- pdf: {
211
- customHelpers: {
212
- // Simple value transformation
213
- customFormat: (value: any) => `[${value}]`,
214
-
215
- // String manipulation
216
- repeat: (str: string, times: number) => str.repeat(times || 1),
217
-
218
- // Block helper with conditional logic
219
- ifEquals: function(this: any, arg1: any, arg2: any, options: any) {
220
- return (arg1 === arg2) ? options.fn(this) : options.inverse(this)
221
- }
222
- }
223
- }
349
+ ### Dates
350
+ ```handlebars
351
+ {{formatDate date}} <!-- short style, e.g. 1/15/24 -->
352
+ {{formatDate date "full"}} <!-- full style, e.g. Monday, January 15, 2024 -->
353
+ {{formatDate date format="long"}} <!-- styles: full | long | medium | short -->
224
354
  ```
225
355
 
226
- Use them in templates:
356
+ ### Numbers
227
357
  ```handlebars
228
- {{customFormat "hello"}} <!-- [hello] -->
229
- {{repeat "" 5}} <!-- ★★★★★ -->
230
- {{#ifEquals status "active"}}Active User{{else}}Inactive User{{/ifEquals}}
358
+ {{formatNumber 1234.56}} <!-- 1,234.56 -->
359
+ {{formatNumber 0.15 style="percent"}} <!-- 15% -->
360
+ {{formatNumber n minimumFractionDigits=2}}
231
361
  ```
232
362
 
233
- ### String Helpers
363
+ > `formatNumber` forwards any hash arguments to
364
+ > [`Intl.NumberFormat`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat).
365
+
366
+ ### Math
234
367
  ```handlebars
235
- {{upper "hello world"}} <!-- HELLO WORLD -->
236
- {{lower "HELLO WORLD"}} <!-- hello world -->
237
- {{capitalize "hello world"}} <!-- Hello world -->
238
- {{truncate "Long text here" 10}} <!-- Long text... -->
368
+ {{add 10 5}} <!-- 15 -->
369
+ {{subtract 10 5}} <!-- 5 -->
370
+ {{multiply 10 5}} <!-- 50 -->
371
+ {{divide 10 5}} <!-- 2 (returns 0 when dividing by 0) -->
372
+ {{percentage 25 100}} <!-- 25.00% -->
373
+ {{lineTotal qty price}} <!-- qty * price, fixed to 2 decimals -->
239
374
  ```
240
375
 
241
- ### Line Calculations
376
+ ### Strings
242
377
  ```handlebars
243
- {{lineTotal quantity price}} <!-- quantity * price -->
378
+ {{upper "hello world"}} <!-- HELLO WORLD -->
379
+ {{lower "HELLO WORLD"}} <!-- hello world -->
380
+ {{capitalize "hello world"}} <!-- Hello world -->
381
+ {{truncate "Long text here" 10}} <!-- Long text ... -->
244
382
  ```
245
383
 
246
- ## Template Features
384
+ ### Comparison
247
385
 
248
- ### Automatic Enrichment
386
+ Work both inline (returning a boolean) and as block helpers:
249
387
 
250
- The module automatically enriches your data based on template names:
388
+ ```handlebars
389
+ {{#if (eq status "paid")}}Paid{{/if}}
390
+ {{#eq a b}}equal{{else}}different{{/eq}}
391
+ {{#ne a b}}not equal{{/ne}}
392
+ {{#gt total 1000}}High value{{/gt}}
393
+ ```
251
394
 
252
- **Invoice Templates** get:
253
- - `subtotal` - Sum of all line items
254
- - `tax` - Calculated tax amount
255
- - `total` - Subtotal + tax
256
- - `dueDate` - Calculated from issue date + payment terms
395
+ ---
257
396
 
258
- **Sales Report Templates** get:
259
- - `quarters` - Quarterly breakdown of sales data
260
- - `rating` - Performance rating based on total sales
397
+ ## Custom Helpers
261
398
 
262
- ### CSS Styling
399
+ Define your own Handlebars helpers in `nuxt.config.ts`. They are compiled into a
400
+ server-side module at build time, so **they work in development *and* production**:
263
401
 
264
- Use standard CSS with print-specific rules:
402
+ ```ts
403
+ pdf: {
404
+ customHelpers: {
405
+ // Simple value transformation
406
+ shout: (value: string) => `${String(value ?? '').toUpperCase()}!`,
265
407
 
266
- ```css
267
- @page {
268
- size: A4;
269
- margin: 20mm;
270
- }
408
+ // Repeat a string
409
+ repeat: (str: string, times: number) => String(str).repeat(times || 1),
271
410
 
272
- .page-break {
273
- page-break-before: always;
411
+ // Block helper with conditional logic
412
+ ifEquals(this: unknown, a: unknown, b: unknown, options: any) {
413
+ return a === b ? options.fn(this) : options.inverse(this)
414
+ },
415
+ },
274
416
  }
417
+ ```
275
418
 
276
- @media print {
277
- .no-break {
278
- page-break-inside: avoid;
279
- }
280
- }
419
+ ```handlebars
420
+ {{shout "hello"}} <!-- HELLO! -->
421
+ {{repeat "★" 5}} <!-- ★★★★★ -->
422
+ {{#ifEquals status "active"}}Active{{else}}Inactive{{/ifEquals}}
281
423
  ```
282
424
 
283
- ### Partials
425
+ > ⚠️ **Custom helpers must be self-contained.** Only each function's *source* is
426
+ > serialized into the runtime module — a helper cannot reference variables, imports,
427
+ > or values from the surrounding `nuxt.config.ts` scope. Keep all logic inside the
428
+ > function body.
429
+
430
+ ---
431
+
432
+ ## Partials
284
433
 
285
- Create reusable components in your `sharedComponents` directory:
434
+ Create reusable fragments in any `sharedComponents` directory:
286
435
 
287
436
  ```handlebars
288
437
  <!-- pdf/partials/header.hbs -->
@@ -292,34 +441,106 @@ Create reusable components in your `sharedComponents` directory:
292
441
  </div>
293
442
  ```
294
443
 
295
- Use in templates:
444
+ Use them in templates, passing parameters as needed:
445
+
296
446
  ```handlebars
297
447
  {{> header title="My Document" subtitle="Generated Report"}}
298
448
  ```
299
449
 
300
- ## API Reference
450
+ ---
451
+
452
+ ## Automatic Data Enrichment
453
+
454
+ For convenience, the module derives some fields based on the **template name**:
455
+
456
+ **Invoice templates** (name contains `invoice`) receive, when `items[]` is present:
457
+
458
+ - `subtotal` — sum of `quantity × price` across items
459
+ - `tax` — `subtotal × (taxRate ?? 0.1)`
460
+ - `total` — `subtotal + tax`
461
+ - `dueDate` — `issueDate + paymentTerms` days (when both are provided)
462
+
463
+ **Sales report templates** (name contains `salesreport`) receive, when `salesData[]`
464
+ is present:
465
+
466
+ - `quarters` — quarterly totals derived from each entry's `date`/`amount`
467
+ - `rating` — performance rating from total sales (`Excellent` / `Good` / `Average` /
468
+ `Needs Improvement`)
469
+
470
+ > Provide these values yourself if you don't want them computed — your data is
471
+ > spread first, then enriched.
301
472
 
302
- ### `usePdf()`
473
+ ---
303
474
 
304
- The main composable for PDF operations:
475
+ ## Composable API `usePdf()`
305
476
 
306
- ```typescript
477
+ ```ts
307
478
  const {
308
- generate, // (template, data, options?, locale?) => Promise<Blob>
309
- download, // (template, data, options?, filename?, locale?) => Promise<void>
479
+ generate, // (template, data, options?, locale?) => Promise<Blob>
480
+ download, // (template, data, options?, filename?, locale?) => Promise<void>
310
481
  getAvailableLocales, // () => string[]
311
- getDefaultLocale // () => string
482
+ getDefaultLocale, // () => string
312
483
  } = usePdf()
313
484
  ```
314
485
 
315
- ### Options
486
+ | Method | Signature | Notes |
487
+ | ------ | --------- | ----- |
488
+ | `generate` | `(template, data, options?, locale?) => Promise<Blob>` | Returns an `application/pdf` `Blob`. |
489
+ | `download` | `(template, data, options?, filename?, locale?) => Promise<void>` | Browser-only — creates and clicks a download link. |
490
+ | `getAvailableLocales` | `() => string[]` | Reads `availableLocales` from public runtime config. |
491
+ | `getDefaultLocale` | `() => string` | Reads `defaultLocale` from public runtime config. |
316
492
 
317
- ```typescript
493
+ When no `locale` is passed, `defaultLocale` is used.
494
+
495
+ ---
496
+
497
+ ## Server API — `POST /api/pdf`
498
+
499
+ The composable calls a server route that you can also hit directly (e.g. from other
500
+ services). The module registers it automatically.
501
+
502
+ **Request**
503
+
504
+ ```http
505
+ POST /api/pdf
506
+ Content-Type: application/json
507
+ ```
508
+
509
+ ```json
510
+ {
511
+ "template": "Invoice",
512
+ "ctx": {
513
+ "data": { "invoiceNumber": "INV-001", "items": [] },
514
+ "options": { "format": "A4" },
515
+ "locale": "en"
516
+ }
517
+ }
518
+ ```
519
+
520
+ **Response** — binary `application/pdf` body.
521
+
522
+ **Status codes**
523
+
524
+ | Code | Meaning |
525
+ | ---- | ------- |
526
+ | `200` | PDF generated; body is the PDF bytes. |
527
+ | `400` | Missing/invalid `template` or `ctx`. |
528
+ | `404` | `template` does not match any discovered `.hbs` file. |
529
+ | `500` | Rendering or provider failure (details are logged server-side, not leaked). |
530
+
531
+ ---
532
+
533
+ ## PDF Options
534
+
535
+ Passed as the `options` argument to `generate()`/`download()` and merged over
536
+ `defaultOptions`:
537
+
538
+ ```ts
318
539
  interface PdfOptions {
319
540
  format?: 'A4' | 'Letter' | 'Legal'
320
541
  margin?: {
321
- top?: number
322
- bottom?: number
542
+ top?: number // millimetres
543
+ bottom?: number
323
544
  left?: number
324
545
  right?: number
325
546
  }
@@ -333,49 +554,69 @@ interface PdfOptions {
333
554
  }
334
555
  ```
335
556
 
336
- ## Development
557
+ ### Print-aware CSS
337
558
 
338
- ### Playground
559
+ Templates are plain HTML/CSS, so you can use print rules directly:
339
560
 
340
- The module includes a full playground application:
561
+ ```css
562
+ @page { size: A4; margin: 20mm; }
341
563
 
342
- ```bash
343
- npm run dev
564
+ .page-break { page-break-before: always; }
565
+
566
+ @media print {
567
+ .no-break { page-break-inside: avoid; }
568
+ }
344
569
  ```
345
570
 
346
- This starts a demo app with sample Invoice and Sales Report templates in multiple languages.
571
+ ---
347
572
 
348
- ### Building
573
+ ## Development
574
+
575
+ This repository uses **pnpm**.
349
576
 
350
577
  ```bash
351
- npm run build
578
+ pnpm install # install dependencies
579
+ pnpm dev # run the playground with HMR
580
+ pnpm dev:build # build the playground (Nitro)
581
+ pnpm build # build the module
582
+ pnpm lint # eslint
583
+ pnpm test:unit # vitest unit tests
584
+ pnpm test:e2e # playwright end-to-end tests
585
+ pnpm test # unit + e2e
352
586
  ```
353
587
 
354
- ### Testing
588
+ The `playground/` app demonstrates Invoice and Sales Report templates in English,
589
+ Spanish, and French, plus a custom helper and provider configuration examples.
355
590
 
356
- ```bash
357
- npm run test
358
- ```
591
+ ---
359
592
 
360
- ## Examples
593
+ ## Troubleshooting
361
594
 
362
- Check out the `playground/` directory for complete examples including:
595
+ **Template `"X"` not found (404).** The `template` must match a `.hbs` filename
596
+ (without extension) inside one of your `components` directories. Remember the lookup
597
+ is by filename, not path.
363
598
 
364
- - **Invoice Template** - Complete invoice with line items, taxes, and totals
365
- - **Sales Report Template** - Comprehensive report with metrics and quarterly breakdown
366
- - **Multi-language Support** - Templates in English, Spanish, and French
367
- - **Multiple Providers** - Configuration examples for all supported providers
599
+ **Puppeteer can't find Chrome.** Either rely on Puppeteer's bundled Chromium (omit
600
+ `executablePath`) or set `CHROME_PATH`/`executablePath` to a valid Chrome binary. In
601
+ containers you'll usually need `args: ['--no-sandbox']`.
368
602
 
369
- ## License
603
+ **Gotenberg/Browserless errors.** Confirm the `url` is reachable from the server and,
604
+ for Browserless, that `apiKey` is set.
370
605
 
371
- MIT
606
+ **My custom helper does nothing.** Ensure it's a function and fully self-contained —
607
+ it cannot close over values defined elsewhere in `nuxt.config.ts` (only the function
608
+ source is serialized).
609
+
610
+ ---
611
+
612
+ ## License
372
613
 
373
- ## Contributing
614
+ [MIT](./LICENSE)
374
615
 
375
- Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
616
+ ---
376
617
 
377
618
  ## Support
378
619
 
379
620
  - 📖 [Documentation](https://github.com/Maistik-Studio/nuxt-pdf)
380
621
  - 🐛 [Issue Tracker](https://github.com/Maistik-Studio/nuxt-pdf/issues)
381
- - 💬 [Discussions](https://github.com/Maistik-Studio/nuxt-pdf/discussions)
622
+ - 💬 [Discussions](https://github.com/Maistik-Studio/nuxt-pdf/discussions)