Quickstart
This walkthrough renders a small invoice in three files: a template, the data, and a Ruby script. It takes about five minutes. It assumes the gem is installed.
#1. Write the template
A template is a JSON object. content is the list of elements laid out from top to bottom, and ${...} placeholders read from the data. Save this as invoice.template.json:
{
"fonts": ["titillium"],
"footer": [
{ "type": "paragraph", "value": "Page #PAGE# of #PAGES#",
"options": { "alignment": "center", "fontSize": 8, "color": "7c808a" } }
],
"content": [
{ "type": "paragraph", "value": "${company.name}",
"options": { "fontSize": 22, "fontWeight": "bold", "color": "0f766e" } },
{ "type": "paragraph", "value": "Invoice ${invoice.number} · ${invoice.date}",
"options": { "fontSize": 11, "spacing": 18 } },
{ "type": "table", "data": "items",
"header_columns": [
{ "text": "Description", "width": 301, "fontWeight": "bold" },
{ "text": "Qty", "width": 60, "alignment": "right", "fontWeight": "bold" },
{ "text": "Amount", "width": 120, "alignment": "right", "fontWeight": "bold" }
],
"rows": [ [
{ "text": "${name}", "width": 301 },
{ "text": "${qty}", "width": 60, "alignment": "right" },
{ "text": "${amount}", "width": 120, "alignment": "right" }
] ],
"options": { "header": { "fillColor": "0f766e", "textColor": "ffffff" },
"stripe": "f1f5f9", "padding_x": 6, "padding_y": 6 } },
{ "type": "move", "y": 12 },
{ "type": "paragraph", "value": "Total due: ${invoice.total}",
"options": { "alignment": "right", "fontSize": 14, "fontWeight": "bold" } }
]
}
A few things to notice:
- The column widths add up to 481 pt. That is the width of an A4 page (595 pt) minus the default margins of 20 mm on each side.
- The table has
"data": "items". Its single template row repeats once for each entry in theitemsarray, and${name}resolves against that entry. #PAGE#and#PAGES#are filled in after pagination, so they are correct on every page.
#2. Build the data
The data is a plain Hash. Format numbers and dates in Ruby: the template prints what you give it.
data = {
'company' => { 'name' => 'Atelier Nord' },
'invoice' => { 'number' => 'AN-2025-0042', 'date' => '28 Nov 2025', 'total' => '€4,800.00' },
'items' => [
{ 'name' => 'Brand identity system', 'qty' => '1', 'amount' => '€2,400.00' },
{ 'name' => 'Signage fabrication (6 panels)', 'qty' => '6', 'amount' => '€1,830.00' },
{ 'name' => 'On-site installation', 'qty' => '1', 'amount' => '€570.00' }
]
}
#3. Render
require 'json'
require 'soli/pdf'
template = JSON.parse(File.read('invoice.template.json'))
Soli::PDF.render_to_file('invoice.pdf',
template: template,
data: data,
title: 'Invoice AN-2025-0042',
author: 'Atelier Nord'
)
puts Soli::PDF.last_warnings # anything the renderer skipped
Open invoice.pdf. That's it.
Tip Paste the template above into the playground, and the data as JSON, to iterate on the layout without running Ruby.
#What just happened
render_to_filecalledSoli::PDF.render, which serialised the template and the data to JSON.- It started the
render_pdfbinary, downloaded on the first run. The template went through a temporary file, and the data went through standard input. - The renderer laid out the page and returned the PDF bytes on standard output. The gem wrote them to
invoice.pdf.
If a placeholder had no value, for example a typo like ${invoice.nubmer}, the render still succeeds: the placeholder renders empty and a warning shows up in Soli::PDF.last_warnings. A template that isn't valid (an unknown element type, broken JSON) raises Soli::PDF::RenderError instead. See Errors & warnings.
#Keep going
- Read a complete template: the starter invoice is about 130 lines with nothing clever in it.
- Put it in a Rails controller: Rails & Rack.
- Learn the elements: Template structure, Tables, Data binding.