The PAGE primitive adds deterministic "Page X of Y" running headers and footers to any multi-page document. It is the engine foundation that every paginated recipe (Statement, Receipt/Report, Certificate) builds on.
What it does
When you author a running header or footer region, Rendro uses a single-pass
substitution to replace {{page_number}} and {{total_pages}} tokens with the
resolved page number and total page count before rendering. The token substitution is
deterministic: given the same input data the same bytes are produced every time.
# docs-contract: page-primitive-basic
data = %{
period: %{from: ~D[2026-05-01], to: ~D[2026-05-31]},
account: %{name: "Acme Corp"},
opening_balance: Decimal.new("1000.00"),
lines: [
%{date: ~D[2026-05-02], description: "Invoice #1", amount: Decimal.new("500.00")},
%{date: ~D[2026-05-15], description: "Payment", amount: Decimal.new("-200.00")}
]
}
doc = Rendro.Recipes.Statement.document(data)
assert doc.page_template == :statement
# The statement recipe wires the PAGE primitive into the running footer region.
# Page X of Y substitution is single-pass and deterministic.
{:ok, pdf} = Rendro.render(doc, deterministic: true)
assert binary_part(pdf, 0, 5) == "%PDF-"Capabilities (bounded by support matrix)
The support matrix row page_numbering records the exact capabilities shipped
in priv/support_matrix.json. The following are supported and backed by
proof in test/rendro/pipeline/paginate_test.exs:
| Capability | Status |
|---|---|
Single-pass {{page_number}} / {{total_pages}} substitution | supported |
| Deterministic output (same input → same bytes) | supported |
First-page suppression via suppress_on | supported |
Use Rendro.page_number/1 to author a running footer or header. Pass
suppress_on: :first to omit the page number on the first page:
# docs-contract: page-primitive-suppress
block = Rendro.page_number(format: "Page {{page_number}} of {{total_pages}}")
# page_number/1 returns a %Rendro.Block{} wrapping a %Rendro.Text{}
assert %Rendro.Block{} = block
assert %Rendro.Text{} = block.content
assert block.content.content =~ "{{page_number}}"
# First-page suppression is applied on the Section level via suppress_on:
section =
Rendro.section(
name: :footer_suppressed,
region: :footer,
suppress_on: :first,
content: [block]
)
assert section.suppress_on == :first"Page X of Y" pattern
The standard running-footer pattern for a billing statement or report:
# Illustrative only — a real recipe assigns section content from data.
Rendro.page_number(format: "Page {{page_number}} of {{total_pages}}")The tokens {{page_number}} and {{total_pages}} are substituted after pagination completes,
so the total page count is always accurate. A single rendering pass is sufficient
— no two-pass layout or back-patching is required.
Scope boundaries
The PAGE primitive does not support:
- Digital signatures or signing preparation (see
priv/support_matrix.jsonunsupportedarray) - Blanket compliance claims (see
unsupportedarray)
These are outside the supported surface. If you need cryptographic signing,
see Rendro.Sign.
Integration with recipes
Every paginated recipe composes the PAGE primitive through the same
Rendro.page_number/1 helper:
Rendro.Recipes.Statement— running footer "Page X of Y" on each statement pageRendro.Recipes.Receipt— running footer on each report pageRendro.Recipes.Certificate— geometry-derived layout; no header/footer regions
See guides/recipes.md for the full per-recipe documentation.