Skip to main content

guides/page_primitive.md

# PAGE Primitive

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.

```elixir
# 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:

```elixir
# 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:

```elixir-schematic
# 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:

- `Rendro.Recipes.Statement` — running footer "Page X of Y" on each statement page
- `Rendro.Recipes.Receipt` — running footer on each report page
- `Rendro.Recipes.Certificate` — geometry-derived layout; no header/footer regions

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