Authoring
The primitives, the styles-not-CSS model, composition with ordinary Vue, and definePdf.
A PDF template is an ordinary Vue Single File Component under pdfs/. You get
typed props, interpolation, v-if, keyed v-for, slots, and local components.
What differs from a page component is the element set and the styling model.
The directory
Templates live in pdfs/**/*.vue. Three sibling directories are reserved and
are not registered as documents:
| Directory | Purpose |
|---|---|
pdfs/components | Local .vue components used by templates |
pdfs/assets | Local PNG/JPEG images (see Images & fonts) |
pdfs/fonts | Local TTF/OTF fonts |
Discovery is deterministic. Nested paths keep slash registry keys
(reports/monthly), and a project template overrides a same-keyed template from
an extended layer.
The primitives
The document primitives are available throughout PDF templates and their descendants inside the isolated PDF renderer app:
| Component | Role |
|---|---|
PdfDocument | Root; carries document metadata and outline mode |
PdfPage | A page with a size, orientation, and padding |
PdfView | A flexbox container (the layout workhorse) |
PdfText | A text run; the only place text may live |
PdfImage | A raster image from a local or embedded source |
PdfLink | An external URL or internal #id link annotation |
PdfNote | A sticky-note annotation |
There is also a full SVG primitive set for vector drawing. Every primitive's props are tabulated in the reference.
PdfText. Non-whitespace text placed directly under a
PdfView or PdfPage fails the render with PDF_TREE_INVALID; Nuxt PDF never
silently drops document content.The prop surface is closed at runtime as well as in TypeScript. Vue can forward
undeclared attributes that escape static checking, so the renderer keeps a
per-primitive allowlist: an unknown prop, a DOM/event attribute, or a valid PDF
prop used on the wrong primitive fails with PDF_TREE_INVALID. There is no
silent passthrough to the engine.
Styles are PDF objects, not CSS
The :style prop takes a typed Nuxt PDF style object, not browser CSS. It
is a familiar subset of flexbox, the box model, colors, and transforms. It is its
own framework-owned system. Nuxt PDF does not re-export the wider,
version-dependent React PDF stylesheet types:
<PdfView
:style="{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 12,
backgroundColor: '#f4f6f4',
borderRadius: 4,
}"
>
<PdfText>Invoice total</PdfText>
</PdfView>Key differences from CSS:
- Properties are camelCase (
backgroundColor,marginTop), never kebab-case. - The default flex direction is
column, as in React Native. It is notrow. - Values are unitless points by default; percentages are strings (
'50%'). - There is no cascade from a global stylesheet. Inheritance flows through the component tree for the properties the engine inherits (font family, size, color), and you set everything else explicitly.
For a reusable style, use satisfies PdfStyle. It checks misspelled keys,
unsupported values, and units without widening the object's inferred type:
<script setup lang="ts">
import type { PdfStyle } from '@lupinum/nuxt-pdf'
const base = {
color: '#18251d',
fontSize: 11,
lineHeight: 1.4,
} satisfies PdfStyle
const total = {
fontWeight: 700,
textAlign: 'right',
} satisfies PdfStyle
</script>
<template>
<PdfText :style="[base, isTotal && total]">
{{ amount }}
</PdfText>
</template>Prefer satisfies PdfStyle to as PdfStyle: an assertion can hide an invalid
property while satisfies reports it at authoring time. The complete key,
value, unit, inheritance, and primitive matrix is in the
style reference.
:style also accepts recursively nested arrays. false, null, and
undefined entries are filtered and the remaining objects are merged
left-to-right:
<PdfText :style="[base, [isTotal && total]]">
{{ amount }}
</PdfText>Composition is ordinary Vue
Break a document into components exactly as you would a page. Keep pieces owned
by one document under pdfs/components/<document>; reserve the components root
for pieces genuinely reused by several documents:
<script setup lang="ts">
defineProps<{ label: string; amount: string }>()
</script>
<template>
<PdfView :style="{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 }">
<PdfText>{{ label }}</PdfText>
<PdfText>{{ amount }}</PdfText>
</PdfView>
</template><script setup lang="ts">
import InvoiceLine from './components/invoice/InvoiceLine.vue'
</script>
<template>
<PdfView>
<InvoiceLine
v-for="line in invoice.lines"
:key="line.id"
:label="line.label"
:amount="line.amount"
/>
</PdfView>
</template>v-if, keyed v-for, and slots all work. Invalid primitive nesting (for
example a PdfPage inside a PdfText) and DOM-only attributes (class,
data-*, aria-*) fail early with a targeted diagnostic naming the template.
Execution model
Each render creates a fresh Vue runtime-core application in Node. Nuxt PDF does not use Vue's server renderer, so the lifecycle is deliberately different from HTML SSR:
- mount, update, and unmount hooks run in Node;
onServerPrefetchis not a PDF data-loading hook;- browser globals such as
windowanddocumentare unavailable; and - the PDF app is unmounted after the render, including when authoring fails.
Load request and application data before calling
pdf.<template>.render(props), then pass it through typed props. Async setup,
top-level await, and defineAsyncComponent are rejected rather than allowing
document content to appear late or disappear silently.
Vue reactivity APIs such as ref and computed, plus
usePdfPageNumbers, are scope-aware auto-imports in PDF SFCs. Nuxt app-context
composables, plugins, app-level provides, global DOM components, and directives
are not inherited by the fresh renderer app. Use explicit local imports and
typed props. Ordinary provide/inject between ancestors and descendants inside
the same PDF tree works normally.
DOM-only rendering concepts do not have PDF equivalents. Teleport and
v-show are rejected; use ordinary local composition and v-if instead.
definePdf is likewise valid only as one top-level compiler-macro call in a
discovered pdfs/*.vue template. If it is called elsewhere, its runtime
fallback throws a targeted error.
Dynamic text
Some content is known only during pagination. The page number is the most common example. A
PdfText with a render function receives per-page context and returns a
string or number:
<PdfText
fixed
:render="({ pageNumber, totalPages }) => `Page ${pageNumber} of ${totalPages}`"
/>render callbacks are synchronous and must return a scalar. They run during
layout, once per page the node appears on. Pair render with fixed to repeat
the node on every page.
lineHeight multiplier is intentionally not applied to it. Apply
lineHeight directly to static PdfText nodes. This is a deliberate divergence
from upstream React PDF. See Conformance.definePdf
Each template calls definePdf once to declare render metadata and
development-only preview data:
definePdf<Props>({
title: props => `Report ${props.id}`,
filename: props => `report-${props.id}.pdf`,
language: 'en-GB',
sampleData: { id: 'sample' },
scenarios: {
long: { id: 'long-report' },
},
})title and filename are a static string or a function of props. A defined
title or language overrides the same metadata on PdfDocument on every
layout pass; an omitted value leaves the document prop intact. sampleData
drives the default preview, and each entry in scenarios becomes a preview tab
and a ?scenario= query. Both preview fields are structurally absent from
production builds and never appear on the public template handle. Full option
semantics are in the
definePdf reference.