@lupinum/nuxt-pdf 0.4.0-beta.4 → 0.4.0-beta.5

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONFORMANCE.md +4 -4
  3. package/README.md +28 -0
  4. package/dist/agent/AGENTS.md +48 -0
  5. package/dist/agent/manifest.json +215 -0
  6. package/dist/agent/pages/docs/examples/supported-behavior.md +75 -0
  7. package/dist/agent/pages/docs/examples.md +163 -0
  8. package/dist/agent/pages/docs/getting-started/installation.md +90 -0
  9. package/dist/agent/pages/docs/getting-started/quickstart.md +126 -0
  10. package/dist/agent/pages/docs/getting-started.md +61 -0
  11. package/dist/agent/pages/docs/guides/contents-links-bookmarks.md +123 -0
  12. package/dist/agent/pages/docs/guides/deployment.md +118 -0
  13. package/dist/agent/pages/docs/guides/errors-and-debugging.md +90 -0
  14. package/dist/agent/pages/docs/guides/images-and-fonts.md +102 -0
  15. package/dist/agent/pages/docs/guides/recipes.md +258 -0
  16. package/dist/agent/pages/docs/guides/remote-images.md +72 -0
  17. package/dist/agent/pages/docs/guides/reusable-components.md +81 -0
  18. package/dist/agent/pages/docs/guides/standalone-node.md +107 -0
  19. package/dist/agent/pages/docs/guides/svg-graphics.md +165 -0
  20. package/dist/agent/pages/docs/guides/testing.md +119 -0
  21. package/dist/agent/pages/docs/learn/document-tree.md +91 -0
  22. package/dist/agent/pages/docs/learn/runtime-and-data.md +63 -0
  23. package/dist/agent/pages/docs/learn/styling-and-layout.md +79 -0
  24. package/dist/agent/pages/docs/learn/templates-and-registry.md +96 -0
  25. package/dist/agent/pages/docs/reference/composables.md +55 -0
  26. package/dist/agent/pages/docs/reference/define-pdf.md +96 -0
  27. package/dist/agent/pages/docs/reference/errors-and-limits.md +89 -0
  28. package/dist/agent/pages/docs/reference/module-options.md +104 -0
  29. package/dist/agent/pages/docs/reference/page-sizes.md +54 -0
  30. package/dist/agent/pages/docs/reference/primitives.md +216 -0
  31. package/dist/agent/pages/docs/reference/registry.md +185 -0
  32. package/dist/agent/pages/docs/reference/standalone.md +53 -0
  33. package/dist/agent/pages/docs/reference/styles.md +176 -0
  34. package/dist/agent/pages/docs/reference/test-utilities.md +124 -0
  35. package/dist/module.json +1 -1
  36. package/dist/module.mjs +1 -1
  37. package/package.json +12 -9
@@ -0,0 +1,102 @@
1
+ ---
2
+ title: "Add images and fonts"
3
+ description: "Bundle local PNG, JPEG, TTF, OTF, and WOFF2 resources with a PDF template."
4
+ url: "https://nuxt-pdf.lupinum.com/docs/guides/images-and-fonts"
5
+ route: "/docs/guides/images-and-fonts"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Add images and fonts
13
+
14
+ > Bundle local PNG, JPEG, TTF, OTF, and WOFF2 resources with a PDF template.
15
+
16
+ Keep local images under `pdfs/assets` and fonts under `pdfs/fonts`. Nuxt PDF
17
+ validates and embeds them in the Nitro server output.
18
+
19
+ In development, each render reads and validates local image files again.
20
+ Image edits take effect without restarting the server.
21
+
22
+ ## Add a local image
23
+
24
+ Put a PNG or JPEG in `pdfs/assets`. Set `PdfImage.src` to the path relative to
25
+ that directory:
26
+
27
+ ```vue
28
+ <PdfImage
29
+ src="brand/logo.png"
30
+ :style="{ height: 40, objectFit: 'contain', width: 120 }"
31
+ />
32
+ ```
33
+
34
+ Public rendering accepts local paths, admitted PNG or JPEG byte sources, and
35
+ allowlisted HTTPS URLs. It blocks `data:` URL strings, absolute filesystem
36
+ paths, parent traversal, symlink escapes, and SVG files used as image sources.
37
+
38
+ Use [`PdfSvg`](/raw/docs/guides/svg-graphics.md) for vector graphics authored in the
39
+ document tree.
40
+
41
+ ## Register a local font
42
+
43
+ Put a TTF, OTF, or WOFF2 file in `pdfs/fonts`, then register one entry for each weight
44
+ and style used by the document:
45
+
46
+ ```ts [nuxt.config.ts]
47
+ export default defineNuxtConfig({
48
+ modules: ['@lupinum/nuxt-pdf'],
49
+ pdf: {
50
+ fonts: [
51
+ {
52
+ family: 'Invoice Sans',
53
+ src: 'InvoiceSans-Regular.ttf',
54
+ fontStyle: 'normal',
55
+ fontWeight: 400,
56
+ },
57
+ {
58
+ family: 'Invoice Sans',
59
+ src: 'InvoiceSans-Bold.ttf',
60
+ fontStyle: 'normal',
61
+ fontWeight: 700,
62
+ },
63
+ ],
64
+ },
65
+ })
66
+ ```
67
+
68
+ Set the family on a parent so text descendants inherit it:
69
+
70
+ ```vue
71
+ <PdfPage :style="{ fontFamily: 'Invoice Sans' }">
72
+ <PdfText>Invoice</PdfText>
73
+ </PdfPage>
74
+ ```
75
+
76
+ If a requested family, weight, or style was not registered, rendering fails
77
+ with `PDF_LAYOUT_ERROR`. If the configured file is invalid, module setup throws
78
+ a `TypeError` before a template render starts.
79
+
80
+ ## Check language coverage
81
+
82
+ A font must contain the glyphs used by the document. Latin Extended, Greek,
83
+ Cyrillic, and custom hyphenation are covered by the tested corpus with supplied
84
+ fonts. CJK, combining marks, Arabic, bidirectional text, and variable fonts need
85
+ application-specific semantic and raster tests. Emoji and fallback font chains
86
+ are not supported; use an admitted image for an emoji graphic.
87
+
88
+ ## Resource validation
89
+
90
+ The module checks four things:
91
+
92
+ - **Structure:** images must decode as PNG or JPEG; fonts need a valid TTF, OTF,
93
+ or WOFF2 structure and matching extension.
94
+ - **Size:** a local image is limited to 10 MB and a font to 5 MB.
95
+ - **Location:** the resolved path must remain inside its declared root.
96
+ - **Embedding:** validated bytes travel in the Nitro server output, so
97
+ production rendering does not read these files from disk.
98
+
99
+ Development reloads local image bytes for the next preview render. The
100
+ [module-options reference](/raw/docs/reference/module-options.md) lists the exact font
101
+ fields. Use the [remote-image guide](/raw/docs/guides/remote-images.md) for HTTPS
102
+ sources.
@@ -0,0 +1,258 @@
1
+ ---
2
+ title: "Production recipes"
3
+ description: "Copyable Vue patterns for invoices, statements, reports, repeating matter, and payment QR codes."
4
+ url: "https://nuxt-pdf.lupinum.com/docs/guides/recipes"
5
+ route: "/docs/guides/recipes"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Production recipes
13
+
14
+ > Copyable Vue patterns for invoices, statements, reports, repeating matter, and payment QR codes.
15
+
16
+ These are application recipes, not extra framework primitives. They compose the
17
+ same small `PdfView`/`PdfText` surface used by the tested playground documents.
18
+
19
+ Use this page as a pattern index:
20
+
21
+ - [Repeating header, footer, and page numbers](#repeating-header-footer-and-page-numbers)
22
+ - [Address and key/value blocks](#address-or-party-block)
23
+ - [Invoice lines, totals, and shared table columns](#invoice-lines-and-totals)
24
+ - [Dot leaders](#dot-leaders-for-contents-and-price-lists)
25
+ - [Payment details and QR graphics](#payment-details-and-qr)
26
+ - [Headings and continuation headers](#keep-a-heading-with-what-follows)
27
+ - [Reports with a table of contents](#report-sections-and-a-table-of-contents)
28
+
29
+ Read [Build reusable document components](/raw/docs/guides/reusable-components.md)
30
+ before turning a recipe into a shared component. Exact style values remain in
31
+ the [style reference](/raw/docs/reference/styles.md).
32
+
33
+ ## Repeating header, footer, and page numbers
34
+
35
+ A `fixed` node repeats when one authored page wraps into multiple output pages.
36
+ Keep the page padding large enough that flowing content never overlaps it.
37
+
38
+ ```vue
39
+ <PdfPage :style="{ paddingBottom: 52, paddingHorizontal: 40, paddingTop: 64 }">
40
+ <PdfView fixed :style="{ left: 40, position: 'absolute', right: 40, top: 24 }">
41
+ <PdfText>Quarterly statement</PdfText>
42
+ </PdfView>
43
+ <PdfText
44
+ fixed
45
+ :render="({ pageNumber, totalPages }) => `Page ${pageNumber} / ${totalPages}`"
46
+ :style="{ bottom: 24, position: 'absolute', right: 40 }"
47
+ />
48
+ <slot />
49
+ </PdfPage>
50
+ ```
51
+
52
+ ## Address or party block
53
+
54
+ Use a typed child component so the invoice template remains readable.
55
+
56
+ ```vue [pdfs/components/PartyBlock.vue]
57
+ <script setup lang="ts">
58
+ defineProps<{ label: string, lines: string[] }>()
59
+ </script>
60
+
61
+ <template>
62
+ <PdfView :style="{ width: 220 }">
63
+ <PdfText :style="{ color: '#68736B', fontSize: 8, marginBottom: 6 }">
64
+ {{ label }}
65
+ </PdfText>
66
+ <PdfText v-for="line in lines" :key="line" :style="{ fontSize: 10 }">
67
+ {{ line }}
68
+ </PdfText>
69
+ </PdfView>
70
+ </template>
71
+ ```
72
+
73
+ ## Key/value details
74
+
75
+ Keep widths explicit; PDF rows should not depend on browser table layout.
76
+
77
+ ```vue
78
+ <PdfView
79
+ v-for="entry in details"
80
+ :key="entry.label"
81
+ :style="{ flexDirection: 'row', marginBottom: 6 }"
82
+ >
83
+ <PdfText :style="{ color: '#68736B', width: 110 }">{{ entry.label }}</PdfText>
84
+ <PdfText :style="{ flex: 1 }">{{ entry.value }}</PdfText>
85
+ </PdfView>
86
+ ```
87
+
88
+ ## Invoice lines and totals
89
+
90
+ Make each line indivisible with `wrap="false"`; keyed rows then move as a unit
91
+ instead of losing a description or amount at a page break.
92
+
93
+ ```vue
94
+ <PdfView
95
+ v-for="line in invoice.lines"
96
+ :key="line.id"
97
+ :wrap="false"
98
+ :style="{ borderBottomColor: '#DDE3DE', borderBottomWidth: 1, flexDirection: 'row', paddingVertical: 10 }"
99
+ >
100
+ <PdfText :style="{ flex: 1 }">{{ line.description }}</PdfText>
101
+ <PdfText :style="{ textAlign: 'right', width: 90 }">{{ money(line.total) }}</PdfText>
102
+ </PdfView>
103
+ <PdfView :wrap="false" :style="{ marginLeft: 'auto', marginTop: 16, width: 220 }">
104
+ <PdfText :style="{ fontWeight: 700, textAlign: 'right' }">Total {{ money(total) }}</PdfText>
105
+ </PdfView>
106
+ ```
107
+
108
+ ## Tables with a shared column spec
109
+
110
+ A table is a column of rows. Give every row the same `flexDirection: 'row'`
111
+ and fixed cell widths, and put those widths in one module. The header and the
112
+ body then cannot drift apart when a column changes.
113
+
114
+ ```ts [pdfs/components/columns.ts]
115
+ export const columns = {
116
+ amount: { align: 'right', width: 86 },
117
+ description: { width: 246 },
118
+ quantity: { align: 'right', width: 52 },
119
+ } as const
120
+ ```
121
+
122
+ ```vue
123
+ <PdfView :style="{ flexDirection: 'row', paddingBottom: 8 }" :wrap="false">
124
+ <PdfText :style="{ fontSize: 7, width: columns.description.width }">Description</PdfText>
125
+ <PdfText :style="{ fontSize: 7, textAlign: 'right', width: columns.quantity.width }">Qty</PdfText>
126
+ <PdfText :style="{ fontSize: 7, textAlign: 'right', width: columns.amount.width }">Amount</PdfText>
127
+ </PdfView>
128
+ <PdfView
129
+ v-for="line in invoice.lines"
130
+ :key="line.id"
131
+ :wrap="false"
132
+ :style="{ borderBottomColor: '#DDE3DE', borderBottomWidth: 1, flexDirection: 'row', paddingVertical: 10 }"
133
+ >
134
+ <PdfText :style="{ flex: 1 }">{{ line.description }}</PdfText>
135
+ <PdfText :style="{ textAlign: 'right', width: columns.amount.width }">{{ money(line.total) }}</PdfText>
136
+ </PdfView>
137
+ ```
138
+
139
+ Keep body cells inside a `wrap="false"` row so a long line moves as a unit.
140
+
141
+ ## Dot leaders for contents and price lists
142
+
143
+ A dotted rule that stretches between a label and a number is one view with a
144
+ dotted bottom border. Wrap each entry in a row with `alignItems: 'flex-end'`
145
+ and let the leader take the remaining space:
146
+
147
+ ```vue [pdfs/components/common/DotLeader.vue]
148
+ <script setup lang="ts">
149
+ defineProps<{
150
+ baseline?: number
151
+ color: string
152
+ gap?: number
153
+ width?: number
154
+ }>()
155
+ </script>
156
+
157
+ <template>
158
+ <PdfView
159
+ :style="{
160
+ borderBottomColor: color,
161
+ borderBottomStyle: 'dotted',
162
+ borderBottomWidth: width ?? 1,
163
+ flex: 1,
164
+ marginBottom: baseline ?? 3,
165
+ marginHorizontal: gap ?? 7,
166
+ }"
167
+ />
168
+ </template>
169
+ ```
170
+
171
+ ```vue
172
+ <PdfView :style="{ alignItems: 'flex-end', flexDirection: 'row' }">
173
+ <PdfText>{{ section.title }}</PdfText>
174
+ <DotLeader color="#CDD5CF" />
175
+ <PdfText>{{ pageNumbers[section.id] ?? '' }}</PdfText>
176
+ </PdfView>
177
+ ```
178
+
179
+ The `baseline` offset sits the dots on the text baseline; start at `2` to `4`
180
+ and adjust against your font size.
181
+
182
+ ## Payment details and QR
183
+
184
+ Generate the payment payload in setup code, turn consecutive dark QR modules
185
+ into one SVG path, and keep the payment panel together with `wrap="false"`.
186
+
187
+ ```vue
188
+ <PdfView :wrap="false" :style="{ flexDirection: 'row', marginTop: 20 }">
189
+ <PdfView :style="{ flex: 1 }">
190
+ <PdfText>IBAN {{ payment.iban }}</PdfText>
191
+ <PdfText>BIC {{ payment.bic }}</PdfText>
192
+ <PdfText>Reference {{ invoice.number }}</PdfText>
193
+ </PdfView>
194
+ <PdfSvg :viewBox="`0 0 ${qrSize} ${qrSize}`" :style="{ height: 72, width: 72 }">
195
+ <PdfPath :d="qrPath" fill="#000000" />
196
+ </PdfSvg>
197
+ </PdfView>
198
+ ```
199
+
200
+ ## Keep a heading with what follows
201
+
202
+ `minPresenceAhead` asks pagination to move the heading when too little usable
203
+ space remains. Use `wrap="false"` only when the whole section is genuinely
204
+ small enough to fit on a page.
205
+
206
+ ```vue
207
+ <PdfView :style="{ marginTop: 24 }">
208
+ <PdfText :min-presence-ahead="54" :style="{ fontSize: 15, marginBottom: 10 }">
209
+ Payment terms
210
+ </PdfText>
211
+ <PdfText>{{ terms }}</PdfText>
212
+ </PdfView>
213
+ ```
214
+
215
+ ## Long rows with continuation headers
216
+
217
+ Put the header and rows in one wrapping page. The fixed header repeats on every
218
+ continuation page; page padding reserves its space.
219
+
220
+ ```vue
221
+ <PdfPage :style="{ paddingHorizontal: 40, paddingTop: 72 }">
222
+ <PdfView fixed :style="{ flexDirection: 'row', left: 40, position: 'absolute', right: 40, top: 36 }">
223
+ <PdfText :style="{ flex: 1 }">Description</PdfText>
224
+ <PdfText :style="{ textAlign: 'right', width: 90 }">Amount</PdfText>
225
+ </PdfView>
226
+ <InvoiceLine v-for="line in lines" :key="line.id" :line="line" />
227
+ </PdfPage>
228
+ ```
229
+
230
+ ## Report sections and a table of contents
231
+
232
+ Read the page map only where the document needs it. The first pass returns no
233
+ number, so keep an empty fallback; Nuxt PDF rerenders until the map stabilizes.
234
+
235
+ ```vue
236
+ <script setup lang="ts">
237
+ const pageNumbers = usePdfPageNumbers()
238
+ </script>
239
+
240
+ <template>
241
+ <PdfPage>
242
+ <PdfLink v-for="section in sections" :key="section.id" :href="`#${section.id}`">
243
+ {{ section.title }} ..... {{ pageNumbers[section.id] ?? '' }}
244
+ </PdfLink>
245
+ </PdfPage>
246
+ <PdfPage v-for="section in sections" :key="section.id">
247
+ <PdfView :id="section.id" :bookmark="section.title">
248
+ <PdfText :style="{ fontSize: 22 }">{{ section.title }}</PdfText>
249
+ <PdfText>{{ section.body }}</PdfText>
250
+ </PdfView>
251
+ </PdfPage>
252
+ </template>
253
+ ```
254
+
255
+ The [links and table-of-contents guide](/raw/docs/guides/contents-links-bookmarks.md)
256
+ explains convergence and invalid destinations. Use the
257
+ [testing guide](/raw/docs/guides/testing.md) to protect document text, links, page
258
+ counts, and reviewed raster output.
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: "Configure remote images"
3
+ description: "Admit specific HTTPS image paths and understand the request boundary."
4
+ url: "https://nuxt-pdf.lupinum.com/docs/guides/remote-images"
5
+ route: "/docs/guides/remote-images"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Configure remote images
13
+
14
+ > Admit specific HTTPS image paths and understand the request boundary.
15
+
16
+ Remote image fetching is off by default. Enable it only for HTTPS locations
17
+ that the application operator controls or trusts.
18
+
19
+ ## Add an allowlist
20
+
21
+ Configure exact URL prefixes with a trailing slash:
22
+
23
+ ```ts [nuxt.config.ts]
24
+ export default defineNuxtConfig({
25
+ modules: ['@lupinum/nuxt-pdf'],
26
+ pdf: {
27
+ remote: {
28
+ allow: [
29
+ 'https://cdn.example.com/brand/',
30
+ 'https://images.example.com/logos/',
31
+ ],
32
+ timeoutMs: 10_000,
33
+ },
34
+ },
35
+ })
36
+ ```
37
+
38
+ Use an admitted URL in the template:
39
+
40
+ ```vue
41
+ <PdfImage
42
+ src="https://cdn.example.com/brand/logo.png"
43
+ :style="{ height: 40, width: 120 }"
44
+ />
45
+ ```
46
+
47
+ The module follows at most three redirects and checks the allowlist at each
48
+ hop. An allowed host cannot redirect to a disallowed location.
49
+
50
+ ## Understand the boundary
51
+
52
+ - Allowlist entries use `https://host/path/`. Wildcards, credentials, query
53
+ strings, fragments, and missing trailing slashes are rejected.
54
+ - Requested image URLs can contain a query, but errors do not include it.
55
+ - Requests use `GET` without application headers, cookies, or credentials.
56
+ - PNG and JPEG signatures and dimensions are checked before layout. A response
57
+ `Content-Type` cannot admit other bytes.
58
+ - Per-image bytes, total bytes, decoded pixels, request count, concurrency, and
59
+ the full render deadline use the shared `pdf.limits` budget.
60
+ - Duplicate URLs are fetched once per render. No cross-render image cache is
61
+ promised.
62
+
63
+ <warning icon="lucide:shield-alert">
64
+ The allowlist controls hosts and paths. It does not provide private-IP or
65
+ DNS-rebinding protection. Do not allow user-controlled hosts. Authenticated
66
+ requests, custom headers, proxies, and remote fonts are not supported.
67
+ </warning>
68
+
69
+ `PDF_ASSET_BLOCKED` means the source form, allowlist, redirect, or timeout
70
+ policy rejected the request. `PDF_ASSET_INVALID` means admitted bytes were not a
71
+ valid PNG or JPEG. Size and request-budget failures use `PDF_LIMIT_EXCEEDED`.
72
+ See [Errors and limits](/raw/docs/reference/errors-and-limits.md) for the exact codes.
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: "Build reusable document components"
3
+ description: "Organize repeated PDF sections without creating a second component system."
4
+ url: "https://nuxt-pdf.lupinum.com/docs/guides/reusable-components"
5
+ route: "/docs/guides/reusable-components"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Build reusable document components
13
+
14
+ > Organize repeated PDF sections without creating a second component system.
15
+
16
+ PDF components are ordinary local Vue components whose rendered roots are Nuxt
17
+ PDF primitives. Extract a component when it owns a meaningful document section
18
+ or repeats in more than one place.
19
+
20
+ ## Keep ownership visible
21
+
22
+ Put document-specific components next to their document family:
23
+
24
+ ```bash [Application files]
25
+ pdfs/
26
+ ├── invoice.vue
27
+ └── components/
28
+ └── invoice/
29
+ ├── InvoiceAddress.vue
30
+ ├── InvoiceLine.vue
31
+ └── columns.ts
32
+ ```
33
+
34
+ Move a component to `pdfs/components/` only when several templates use it. This
35
+ keeps invoice-specific decisions out of a generic component API.
36
+
37
+ ## Pass typed document data
38
+
39
+ ```vue [pdfs/components/invoice/InvoiceAddress.vue]
40
+ <script setup lang="ts">
41
+ defineProps<{
42
+ label: string
43
+ lines: string[]
44
+ }>()
45
+ </script>
46
+
47
+ <template>
48
+ <PdfView>
49
+ <PdfText :style="{ fontSize: 8, marginBottom: 4 }">
50
+ {{ label }}
51
+ </PdfText>
52
+ <PdfText v-for="line in lines" :key="line">
53
+ {{ line }}
54
+ </PdfText>
55
+ </PdfView>
56
+ </template>
57
+ ```
58
+
59
+ Import it directly in the template. Use keyed `v-for`, slots, and `v-if` as you
60
+ would in another Vue component. Do not pass DOM attributes such as `class` or
61
+ `aria-*`; the PDF tree has no DOM.
62
+
63
+ ## Share table geometry
64
+
65
+ Put widths used by both a header and row in one TypeScript module:
66
+
67
+ ```ts [pdfs/components/invoice/columns.ts]
68
+ export const invoiceColumns = {
69
+ description: '58%',
70
+ quantity: '14%',
71
+ price: '14%',
72
+ total: '14%',
73
+ } as const
74
+ ```
75
+
76
+ A table is a column of rows. Give the header and every row the same
77
+ `flexDirection: 'row'` and column widths. The header and body then cannot drift
78
+ apart.
79
+
80
+ The [production recipes](/raw/docs/guides/recipes.md) show address blocks, shared
81
+ columns, totals, continuation headers, and table-of-contents patterns.
@@ -0,0 +1,107 @@
1
+ ---
2
+ title: "Render outside Nitro"
3
+ description: "Compile PDF templates once and render the generated registry in a Node worker."
4
+ url: "https://nuxt-pdf.lupinum.com/docs/guides/standalone-node"
5
+ route: "/docs/guides/standalone-node"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Render outside Nitro
13
+
14
+ > Compile PDF templates once and render the generated registry in a Node worker.
15
+
16
+ Use the standalone build when a Node worker owns document rendering outside
17
+ Nitro. Keep the Nuxt module for authoring and preview. Both paths use the same
18
+ compiler, resource admission, registry, and PDF engine.
19
+
20
+ ## Build the registry
21
+
22
+ Keep trusted templates in `pdfs/` at the application root. Import application
23
+ helpers explicitly with relative paths. The standalone build does not read
24
+ Nuxt configuration, aliases, auto-imports, or layer configuration. PDF primitives,
25
+ `definePdf`, and `usePdfPageNumbers` remain available in templates.
26
+ Package imports must provide compiled JavaScript. Do not import source-only
27
+ packages or add runtime filesystem reads to templates or their helpers.
28
+
29
+ ```ts [scripts/build-pdfs.mjs]
30
+ import { buildPdfRegistry } from '@lupinum/nuxt-pdf/build'
31
+
32
+ await buildPdfRegistry({
33
+ rootDir: process.cwd(),
34
+ outDir: './generated/pdfs',
35
+ fonts: [{ family: 'Invoice Sans', src: 'Roboto-Regular.ttf' }],
36
+ limits: { maxPages: 100, maxOutputBytes: 8_000_000 },
37
+ })
38
+ ```
39
+
40
+ Put the configured font in `pdfs/fonts/`. Put local images in `pdfs/assets/`.
41
+ Remove the `fonts` option if the templates use only built-in fonts.
42
+
43
+ Run this script before the backend build:
44
+
45
+ ```bash
46
+ node scripts/build-pdfs.mjs
47
+ ```
48
+
49
+ The output directory contains `index.mjs`, `index.d.mts`, a `types/` declaration
50
+ tree, and an ownership marker. Deploy the generated runtime and its package
51
+ dependencies. Keep the declarations for backend type checking. No Vue source,
52
+ test loader, or build compiler is needed to execute this generated registry.
53
+ The build tools remain package dependencies but are not imported by the runtime.
54
+
55
+ Use a dedicated output directory inside the application. Never put hand-written
56
+ files in it. Successful rebuilds replace its generated contents. Compilation
57
+ failures preserve the previous output. An existing nonempty directory without
58
+ the ownership marker is rejected. Choose another directory after this error.
59
+ Do not run concurrent builds into one output directory.
60
+
61
+ ## Render validated data
62
+
63
+ The generated `pdf`, `renderPdf`, `getPdfTemplate`, and `pdfTemplateKeys` exports
64
+ have the same behavior as the [Nitro registry](/raw/docs/reference/registry.md).
65
+ Import the `.mjs` path. Ordinary TypeScript resolves its adjacent declarations.
66
+
67
+ ```ts [workers/render-invoice.ts]
68
+ import { pdf } from '../generated/pdfs/index.mjs'
69
+
70
+ export async function renderInvoice() {
71
+ const result = await pdf.invoice.render({
72
+ customer: 'Example customer',
73
+ number: '2026-001',
74
+ lines: [],
75
+ })
76
+ return result.toUint8Array()
77
+ }
78
+ ```
79
+
80
+ Match the props to your own invoice template. Validate request data and check
81
+ authorization before rendering. Props are statically typed, not a runtime
82
+ validation schema. Never accept template source, filesystem paths, build
83
+ options, or unrestricted image URLs from a caller.
84
+
85
+ Local assets and configured fonts are validated and embedded during the build.
86
+ Remote images remain denied unless the build supplies an explicit `remote`
87
+ policy. Existing resource limits and error codes apply to each render. Fix the
88
+ named resource or lower input size after an admission or limit error.
89
+
90
+ The application owns jobs, snapshots, storage, document issuance, and delivery.
91
+ For an issued document, store and reuse completed bytes. Two fresh renders are
92
+ not promised to be byte-identical.
93
+
94
+ ## Verify the deployment
95
+
96
+ Standalone Node rendering is tested locally with embedded fonts and images,
97
+ Unicode text, pagination, and concurrent requests. That is not a deployed
98
+ Convex or other serverless-runtime certification. Edge workers remain unsupported.
99
+
100
+ Before adoption, measure your largest expected document on the target runtime.
101
+ Check cold and warm duration, memory use, output limits, failed asset requests,
102
+ and concurrent isolation. Keep the previously tested Node route available for
103
+ new renders until this proof passes. Do not regenerate issued PDFs during a
104
+ runtime migration.
105
+
106
+ See [deployment guidance](/raw/docs/guides/deployment.md) for the runtime matrix and
107
+ operational limits.