@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/LICENSE +21 -0
- package/README.md +427 -186
- package/dist/module.d.mts +2 -2
- package/dist/module.d.ts +2 -2
- package/dist/module.json +4 -4
- package/dist/module.mjs +20 -8
- package/dist/runtime/composables/usePdf.d.ts +2 -2
- package/dist/runtime/server/api/pdf.post.js +38 -12
- package/dist/runtime/types.d.ts +3 -3
- package/dist/runtime/utils/compiler.d.ts +4 -3
- package/dist/runtime/utils/compiler.js +48 -39
- package/dist/runtime/utils/providers.d.ts +2 -2
- package/dist/runtime/utils/providers.js +46 -10
- package/package.json +33 -31
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
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@maistik/nuxt-pdf)
|
|
8
|
+
[](https://www.npmjs.com/package/@maistik/nuxt-pdf)
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
[](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
|
|
8
|
-
- 🌍 **Internationalization**
|
|
9
|
-
- 🔄 **Provider
|
|
10
|
-
- 🎯 **SSR
|
|
11
|
-
- 🧩 **Composable API**
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
-
|
|
53
|
+
---
|
|
16
54
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
+
### 1. Register the module
|
|
26
90
|
|
|
27
|
-
```
|
|
91
|
+
```ts
|
|
92
|
+
// nuxt.config.ts
|
|
28
93
|
export default defineNuxtConfig({
|
|
29
94
|
modules: ['@maistik/nuxt-pdf'],
|
|
30
95
|
pdf: {
|
|
31
|
-
provider: 'puppeteer', //
|
|
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'
|
|
101
|
+
availableLocales: ['en', 'es'],
|
|
37
102
|
i18nMessages: {
|
|
38
|
-
en: {
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
147
|
+
async function makeInvoice() {
|
|
103
148
|
const data = {
|
|
104
149
|
invoiceNumber: 'INV-001',
|
|
105
|
-
items: [
|
|
106
|
-
|
|
107
|
-
],
|
|
108
|
-
total: 100
|
|
150
|
+
items: [{ description: 'Service', price: 100 }],
|
|
151
|
+
total: 100,
|
|
109
152
|
}
|
|
110
|
-
|
|
111
|
-
//
|
|
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
|
-
|
|
241
|
+
Switch backends by changing `provider` and supplying the matching `providers` entry.
|
|
123
242
|
|
|
124
|
-
|
|
243
|
+
### Puppeteer (local / on-prem)
|
|
125
244
|
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
276
|
+
```ts
|
|
145
277
|
pdf: {
|
|
146
278
|
provider: 'gotenberg',
|
|
147
279
|
providers: {
|
|
148
|
-
gotenberg: {
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
280
|
+
gotenberg: { url: 'http://gotenberg:3000' },
|
|
281
|
+
},
|
|
152
282
|
}
|
|
153
283
|
```
|
|
154
284
|
|
|
155
|
-
### Browserless (
|
|
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
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
```handlebars
|
|
175
|
-
{{t "invoice.title"}} <!-- Outputs localized text -->
|
|
176
|
-
```
|
|
307
|
+
---
|
|
177
308
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{{
|
|
181
|
-
|
|
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
|
-
{{
|
|
187
|
-
{{formatDate date "full"}} <!-- Wednesday, December 25, 2024 -->
|
|
322
|
+
<h1>{{t "invoice.title"}}</h1>
|
|
188
323
|
```
|
|
189
324
|
|
|
190
|
-
|
|
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
|
-
{{
|
|
193
|
-
{{formatNumber 0.15 style="percent"}} <!-- 15% -->
|
|
339
|
+
{{t "invoice.title"}} <!-- localized text (or the key if missing) -->
|
|
194
340
|
```
|
|
195
341
|
|
|
196
|
-
###
|
|
342
|
+
### Currency
|
|
197
343
|
```handlebars
|
|
198
|
-
{{
|
|
199
|
-
{{
|
|
200
|
-
{{
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
356
|
+
### Numbers
|
|
227
357
|
```handlebars
|
|
228
|
-
{{
|
|
229
|
-
{{
|
|
230
|
-
{{
|
|
358
|
+
{{formatNumber 1234.56}} <!-- 1,234.56 -->
|
|
359
|
+
{{formatNumber 0.15 style="percent"}} <!-- 15% -->
|
|
360
|
+
{{formatNumber n minimumFractionDigits=2}}
|
|
231
361
|
```
|
|
232
362
|
|
|
233
|
-
|
|
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
|
-
{{
|
|
236
|
-
{{
|
|
237
|
-
{{
|
|
238
|
-
{{
|
|
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
|
-
###
|
|
376
|
+
### Strings
|
|
242
377
|
```handlebars
|
|
243
|
-
{{
|
|
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
|
-
|
|
384
|
+
### Comparison
|
|
247
385
|
|
|
248
|
-
|
|
386
|
+
Work both inline (returning a boolean) and as block helpers:
|
|
249
387
|
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
- `quarters` - Quarterly breakdown of sales data
|
|
260
|
-
- `rating` - Performance rating based on total sales
|
|
397
|
+
## Custom Helpers
|
|
261
398
|
|
|
262
|
-
|
|
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
|
-
|
|
402
|
+
```ts
|
|
403
|
+
pdf: {
|
|
404
|
+
customHelpers: {
|
|
405
|
+
// Simple value transformation
|
|
406
|
+
shout: (value: string) => `${String(value ?? '').toUpperCase()}!`,
|
|
265
407
|
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
419
|
+
```handlebars
|
|
420
|
+
{{shout "hello"}} <!-- HELLO! -->
|
|
421
|
+
{{repeat "★" 5}} <!-- ★★★★★ -->
|
|
422
|
+
{{#ifEquals status "active"}}Active{{else}}Inactive{{/ifEquals}}
|
|
281
423
|
```
|
|
282
424
|
|
|
283
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
473
|
+
---
|
|
303
474
|
|
|
304
|
-
|
|
475
|
+
## Composable API — `usePdf()`
|
|
305
476
|
|
|
306
|
-
```
|
|
477
|
+
```ts
|
|
307
478
|
const {
|
|
308
|
-
generate,
|
|
309
|
-
download,
|
|
479
|
+
generate, // (template, data, options?, locale?) => Promise<Blob>
|
|
480
|
+
download, // (template, data, options?, filename?, locale?) => Promise<void>
|
|
310
481
|
getAvailableLocales, // () => string[]
|
|
311
|
-
getDefaultLocale
|
|
482
|
+
getDefaultLocale, // () => string
|
|
312
483
|
} = usePdf()
|
|
313
484
|
```
|
|
314
485
|
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
557
|
+
### Print-aware CSS
|
|
337
558
|
|
|
338
|
-
|
|
559
|
+
Templates are plain HTML/CSS, so you can use print rules directly:
|
|
339
560
|
|
|
340
|
-
|
|
561
|
+
```css
|
|
562
|
+
@page { size: A4; margin: 20mm; }
|
|
341
563
|
|
|
342
|
-
|
|
343
|
-
|
|
564
|
+
.page-break { page-break-before: always; }
|
|
565
|
+
|
|
566
|
+
@media print {
|
|
567
|
+
.no-break { page-break-inside: avoid; }
|
|
568
|
+
}
|
|
344
569
|
```
|
|
345
570
|
|
|
346
|
-
|
|
571
|
+
---
|
|
347
572
|
|
|
348
|
-
|
|
573
|
+
## Development
|
|
574
|
+
|
|
575
|
+
This repository uses **pnpm**.
|
|
349
576
|
|
|
350
577
|
```bash
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
npm run test
|
|
358
|
-
```
|
|
591
|
+
---
|
|
359
592
|
|
|
360
|
-
##
|
|
593
|
+
## Troubleshooting
|
|
361
594
|
|
|
362
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
603
|
+
**Gotenberg/Browserless errors.** Confirm the `url` is reachable from the server and,
|
|
604
|
+
for Browserless, that `apiKey` is set.
|
|
370
605
|
|
|
371
|
-
|
|
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
|
-
|
|
614
|
+
[MIT](./LICENSE)
|
|
374
615
|
|
|
375
|
-
|
|
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)
|