Skip to main content

The #pdf registry

The typed server module for pdf, renderPdf, the unsafe escape hatch, and the render result.

Nuxt generates an inspectable #pdf module from the discovered templates. It is a server-only module for Nitro code and is typed from each SFC's props. Do not import it from a client component or other browser code.

ts
import { pdf, renderPdf, pdfTemplateKeys } from '#pdf'

pdf

pdf is an object keyed by template name. Property access infers each SFC's props. Every value has exactly three public members:

ts
interface PdfTemplate<Props extends object> {
  readonly key: string
  resolveMetadata(props: Props): {
    title?: string
    filename?: string
    language?: string
  }
  render(props: Props): Promise<PdfRenderResult>
}

The handle deliberately does not expose the compile-time definition, preview fixtures, source file, or a second preview render method.

ts
await pdf.invoice.render({ invoice })
await pdf.report.render({ report })

Nested template paths keep their slash keys (for example pdf['reports/monthly']).

renderPdf

renderPdf(name, props) is the functional form. A literal template name infers its props:

ts
await renderPdf('invoice', { invoice })

A runtime string cannot be type-checked, so it requires the explicit unknown-props escape hatch:

ts
await renderPdf(templateName, untypedProps, { unsafe: true })
{ unsafe: true } does not validate the props; it only makes the loss of static typing visible at the call site. An unknown template name still fails with PDF_TEMPLATE_NOT_FOUND.

pdfTemplateKeys is the array of discovered template names.

getPdfTemplate(name) returns that typed three-member handle for a literal name, or undefined for an unknown name. Reach for it when you need resolved definePdf metadata without rendering:

ts
import { getPdfTemplate } from '#pdf'

const template = getPdfTemplate('invoice')
const { title, filename } = template.resolveMetadata(props)

resolveMetadata() evaluates the title and filename declared by definePdf, and returns its language. It does not mount the component, so an authored PdfDocument fallback is not part of this result.

NuxtPdfError and PDF_ERROR_CODES are also re-exported from #pdf, so server code can branch on render failures without extra imports. See Errors & debugging.

The render result

Both pdf.name.render(props) and renderPdf(...) resolve to a PdfRenderResult. The document is rendered once; every conversion reuses that completed, immutable byte buffer:

ts
const result = await pdf.invoice.render({ invoice })

result.metadata        // frozen title, filename, and language for this render
await result.toUint8Array() // Promise<Uint8Array>
await result.toBuffer()     // Promise<Buffer>
await result.response()     // Promise<Response>

result.metadata is the frozen title, filename, and language resolved for this render: definePdf values win when present, otherwise authored PdfDocument fallbacks. title and language describe PDF Info fields; filename is response/download metadata and is not written into PDF Info. result.diagnostics is one frozen object shared with the development preview:

FieldMeaning
durationMsWall time for the whole public render, including metadata evaluation
byteLengthExact output byte count
pageCountNumber of rendered pages
passesLayout passes used (1 normally, more for a TOC)
registeredFontFacesFrozen family/weight/style facts; never source bytes or paths

Diagnostics deliberately contain no document content, props, or resource URLs, so they are safe to send to an application metrics layer.

Development and production boundary

The development preview keeps its sampleData, named scenarios, and source file in an internal sidecar. It calls the same public render(props) method as application code, reads the viewer title from that exact result's frozen metadata, and parks the completed result for the iframe. There is no separate preview renderer, second metadata evaluation, or second document render.

In a production build the SFC transform reconstructs the render definition from only title, filename, language, and maxPasses. sampleData and scenarios are structurally omitted before Nitro bundles the template; they are not public handle properties and are not merely hidden behind runtime checks. The development sidecar and preview routes are also absent.

The production fixture protects this boundary with canary values inside its preview data. Its build test recursively scans the emitted Nitro server artifact and fails if either canary or a preview-only API token is present. A separate bundle check keeps the renderer and template SFCs out of the client output.

Metadata is hoisted: inline values and imports are supported, while references to bindings declared locally in <script setup> are compile errors. Imported preview-data modules must be side-effect-free so their unused production imports can be eliminated.

response(init)

response() returns a Response with content-type: application/pdf, the exact content-length, and a sanitized, encoded-length-bounded content-disposition filename:

ts
await result.response({
  disposition: 'inline',      // 'attachment' (default) | 'inline'
  filename: 'invoice.pdf',    // sanitized before writing the header
  headers: { 'cache-control': 'no-store' },
})
FieldTypeDescription
disposition'attachment' | 'inline'content-disposition type; defaults to attachment
filenamestringDownload filename; sanitized before use; defaults to template metadata or document.pdf
headersHeadersInitExtra response headers merged in
status, statusTextnumber / stringStandard ResponseInit fields

Caller-provided content-type, content-length, and content-disposition headers are replaced with the truthful values from the completed result.

Regenerating the registry

The registry is written during nuxt prepare and on template changes in dev. If #pdf imports appear untyped right after enabling the module, restart nuxt dev or run nuxt prepare once.