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:

CapabilityStatus
Single-pass {{page_number}} / {{total_pages}} substitutionsupported
Deterministic output (same input → same bytes)supported
First-page suppression via suppress_onsupported

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.json unsupported array)
  • Blanket compliance claims (see unsupported array)

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:

See guides/recipes.md for the full per-recipe documentation.