Layers & renderingLast updated on
Last updated on
How artifacts become output documents
Artifacts define structure. Layers define how that structure becomes output.
A single artifact can have multiple layers—Markdown for AI agents, PDF for printing, HTML for web display—all representing the same underlying data.
What Layers Are
A layer is a view of an artifact. It combines a template with the artifact's data to produce output.
Layers are not transformations—they don't change the artifact. They're representations: different ways of looking at the same source of truth.
const form = p.form()
.name("invoice")
.fields({
customer: { type: "text" },
total: { type: "money" },
})
.defaultLayer("markdown")
.layers({
markdown: p.layer().inline().mimeType("text/markdown").text(`
# Invoice
Customer: {{fields.customer}}
Total: {{fields.total}}
`),
pdf: p.layer().file().mimeType("application/pdf").path("templates/invoice.pdf"),
})
.build()Inline vs File Layers
Inline layers define the template directly in the artifact—good for simple templates or keeping everything in one place.
File layers reference an external template file—good for complex templates or when designers maintain templates separately.
A React layer (text/tsx or text/jsx) must be a file layer. Its path names the composition module, so an inline React layer fails validation.
Templates and Bindings
Text-based layers (Markdown, HTML, plain text) and DOCX layers use Paradoc
templates. Everything inside a marker is an artifact expression:
{{fields.fieldName}} inserts a formatted value, {{#if fields.total > 1000}}
tests a boolean condition, and {{#each parties.tenant}} loops with item as
the current row. See Text layers.
PDF layers work differently. A PDF template has AcroForm fields rather than
placeholders, so a PDF layer declares bindings: each key is an AcroForm
field name in the template, and each value is the Paradoc path that fills it.
Only PDF layers take bindings and bindingsFrom; validation rejects them on
any other layer.
pdf: p.layer()
.file()
.mimeType("application/pdf")
.path("templates/lease.pdf")
.bindings({
"topmostSubform[0].Page1[0].TenantName[0]": "fields.tenantName",
})Resolution
When layers reference external files, resolvers tell Paradoc how to load them. This makes Paradoc environment-agnostic—the same artifact works in Node, browsers, or custom environments.
A resolver is bound once, when the artifact is built or loaded. Every instance
you derive from it afterward—every fill, every render, every mutator—carries
that same resolver, so render calls don't repeat it.
import { createFsResolver } from "@paradoc/resolvers/fs";
const form = p.form()
.name("invoice")
// ...fields, layers...
.build({ resolver: createFsResolver({ root: "./templates" }) })
const output = await form.render({ layer: "pdf" })See Resolvers for available resolvers and how to write custom ones.
Formatting
When rendering, field values are converted to strings. A money value becomes "$1,500.00", a date becomes "Mar 15, 2024".
One immutable formatter controls value presentation for an artifact. Select a precise locale and compose any value-specific policy explicitly.
const output = await form.render({
renderer: createLayerRenderer({ formatter: createFormatter({ locale: 'de-DE' }) })
})See Formatting for details on formatting and localization.
Renderers
Paradoc selects the built-in implementation from the layer's MIME type. The same API renders Markdown, HTML, plain text, PDF, or DOCX, so applications do not need their own format switch.
const markdown = await filled.render({ layer: 'markdown' })
const pdf = await filled.render({ layer: 'pdf' })Pass a custom renderer or configured createLayerRenderer() through the optional
renderer property. Format entry points remain available for low-level
operations. See Renderers for the full API.
React Layers
A React layer points at a React component in a .tsx or .jsx file. Core does
not render React itself, so pass a renderer for the React MIME types in
renderers. reactLayerRenderers() from @paradoc/react-pdf builds the
entries for both text/tsx and text/jsx.
import { reactLayerRenderers } from "@paradoc/react-pdf";
import Invoice from "./invoice.tsx";
const form = p.form()
.name("invoice")
.fields({ customer: { type: "text" } })
.layers({
composition: p.layer().file().mimeType("text/tsx").path("./invoice.tsx"),
})
.build()
const pdf = await form.fill({ fields: { customer: "Acme" } }).render({
layer: "composition",
renderers: reactLayerRenderers({ components: { "./invoice.tsx": Invoice } }),
})components binds the layer's path to a component that is already imported,
so a bundled application never loads the module at render time. Without a
registered renderer, rendering a React layer throws
UnregisteredLayerRendererError.