Production recipes
Copyable Vue patterns for invoices, statements, reports, repeating matter, and payment QR codes.
These are application recipes, not extra framework primitives. They compose the
same small PdfView/PdfText surface used by the tested playground documents.
Repeating header, footer, and page numbers
A fixed node repeats when one authored page wraps into multiple output pages.
Keep the page padding large enough that flowing content never overlaps it.
<PdfPage :style="{ paddingBottom: 52, paddingHorizontal: 40, paddingTop: 64 }">
<PdfView fixed :style="{ left: 40, position: 'absolute', right: 40, top: 24 }">
<PdfText>Quarterly statement</PdfText>
</PdfView>
<PdfText
fixed
:render="({ pageNumber, totalPages }) => `Page ${pageNumber} / ${totalPages}`"
:style="{ bottom: 24, position: 'absolute', right: 40 }"
/>
<slot />
</PdfPage>Address or party block
Use a typed child component so the invoice template remains readable.
<script setup lang="ts">
defineProps<{ label: string, lines: string[] }>()
</script>
<template>
<PdfView :style="{ width: 220 }">
<PdfText :style="{ color: '#68736B', fontSize: 8, marginBottom: 6 }">
{{ label }}
</PdfText>
<PdfText v-for="line in lines" :key="line" :style="{ fontSize: 10 }">
{{ line }}
</PdfText>
</PdfView>
</template>Key/value details
Keep widths explicit; PDF rows should not depend on browser table layout.
<PdfView
v-for="entry in details"
:key="entry.label"
:style="{ flexDirection: 'row', marginBottom: 6 }"
>
<PdfText :style="{ color: '#68736B', width: 110 }">{{ entry.label }}</PdfText>
<PdfText :style="{ flex: 1 }">{{ entry.value }}</PdfText>
</PdfView>Invoice lines and totals
Make each line indivisible with wrap="false"; keyed rows then move as a unit
instead of losing a description or amount at a page break.
<PdfView
v-for="line in invoice.lines"
:key="line.id"
:wrap="false"
:style="{ borderBottomColor: '#DDE3DE', borderBottomWidth: 1, flexDirection: 'row', paddingVertical: 10 }"
>
<PdfText :style="{ flex: 1 }">{{ line.description }}</PdfText>
<PdfText :style="{ textAlign: 'right', width: 90 }">{{ money(line.total) }}</PdfText>
</PdfView>
<PdfView :wrap="false" :style="{ marginLeft: 'auto', marginTop: 16, width: 220 }">
<PdfText :style="{ fontWeight: 700, textAlign: 'right' }">Total {{ money(total) }}</PdfText>
</PdfView>Payment details and QR
Generate the payment payload in setup code, turn consecutive dark QR modules
into one SVG path, and keep the payment panel together with wrap="false".
<PdfView :wrap="false" :style="{ flexDirection: 'row', marginTop: 20 }">
<PdfView :style="{ flex: 1 }">
<PdfText>IBAN {{ payment.iban }}</PdfText>
<PdfText>BIC {{ payment.bic }}</PdfText>
<PdfText>Reference {{ invoice.number }}</PdfText>
</PdfView>
<PdfSvg :viewBox="`0 0 ${qrSize} ${qrSize}`" :style="{ height: 72, width: 72 }">
<PdfPath :d="qrPath" fill="#000000" />
</PdfSvg>
</PdfView>Keep a heading with what follows
minPresenceAhead asks pagination to move the heading when too little usable
space remains. Use wrap="false" only when the whole section is genuinely
small enough to fit on a page.
<PdfView :style="{ marginTop: 24 }">
<PdfText :min-presence-ahead="54" :style="{ fontSize: 15, marginBottom: 10 }">
Payment terms
</PdfText>
<PdfText>{{ terms }}</PdfText>
</PdfView>Long rows with continuation headers
Put the header and rows in one wrapping page. The fixed header repeats on every continuation page; page padding reserves its space.
<PdfPage :style="{ paddingHorizontal: 40, paddingTop: 72 }">
<PdfView fixed :style="{ flexDirection: 'row', left: 40, position: 'absolute', right: 40, top: 36 }">
<PdfText :style="{ flex: 1 }">Description</PdfText>
<PdfText :style="{ textAlign: 'right', width: 90 }">Amount</PdfText>
</PdfView>
<InvoiceLine v-for="line in lines" :key="line.id" :line="line" />
</PdfPage>Report sections and a table of contents
Read the page map only where the document needs it. The first pass returns no number, so keep an empty fallback; Nuxt PDF rerenders until the map stabilizes.
<script setup lang="ts">
const pageNumbers = usePdfPageNumbers()
</script>
<template>
<PdfPage>
<PdfLink v-for="section in sections" :key="section.id" :src="`#${section.id}`">
{{ section.title }} ..... {{ pageNumbers[section.id] ?? '' }}
</PdfLink>
</PdfPage>
<PdfPage v-for="section in sections" :key="section.id">
<PdfView :id="section.id" :bookmark="section.title">
<PdfText :style="{ fontSize: 22 }">{{ section.title }}</PdfText>
<PdfText>{{ section.body }}</PdfText>
</PdfView>
</PdfPage>
</template>The canonical invoice, long-line scenario, annual report, and e-book tests cover these patterns with semantic, pagination, geometry, and reviewed raster checks.