Skip to content

Document Mail Merge

Aurghyadip edited this page Aug 3, 2026 · 1 revision

Document Mail Merge

The #mail-merge(...) engine is designed to generate individual multi-page or single-page documents per recipient record (e.g. letters, invoices, certificates, and reports).


⚙️ Function Signature

#mail-merge(
  data,
  template,
  filter: none,
  sort-by: none,
  reverse: false,
  start: 1,
  limit: none,
  pagebreak: true,
  reset-page-counter: false,
  trim: true,
  default-value: "",
  on-empty: [No matching records found.]
)

🔑 Key Features & Options

1. Pagebreak Control (pagebreak)

By default (pagebreak: true), #mail-merge inserts a #pagebreak() after rendering each record except the last one.

#mail-merge(
  data,
  pagebreak: true,
  record => [ ... ]
)

2. Page Counter Reset (reset-page-counter)

When printing multi-page documents (like 2-page invoices or contracts per client), you often want each recipient's document to start at page 1.

Setting reset-page-counter: true executes counter(page).update(1) at the start of each record iteration:

#set page(
  paper: "us-letter",
  footer: context [
    #align(center)[Page #counter(page).display() of #counter(page).final().at(0)]
  ]
)

#mail-merge(
  data,
  reset-page-counter: true,
  record => [
    // Page 1 of recipient document
    ...
    #pagebreak()
    // Page 2 of recipient document
    ...
  ]
)

3. Record Metadata Helpers

Inside your template closure record => [ ... ], every record dictionary is automatically enriched with internal helper metadata:

Field Key Type Description Helper Function
_index int 1-based index of current record record-index(record)
_zero_index int 0-based index of current record N/A
_total int Total count of filtered records record-total(record)
_is_first bool True if this is the first record is-first-record(record)
_is_last bool True if this is the last record is-last-record(record)

Example Usage of Record Metadata:

#import "@preview/mailmerge:0.1.0": mail-merge, record-index, record-total

#mail-merge(
  data,
  record => [
    #align(right)[Document #record-index(record) of #record-total(record)]
    ...
  ]
)

📭 Handling Empty Datasets (on-empty)

If filtering returns zero matching records, #mail-merge renders the on-empty content parameter instead of crashing or generating an empty PDF.

#mail-merge(
  data,
  filter: record => record.Status == "VIP",
  on-empty: [
    #align(center)[
      *No VIP records found in the current dataset.*
    ]
  ],
  record => [ ... ]
)

Clone this wiki locally