Getting started

Getting started

Install the gem, write a document class, call to_pdf. No fonts, binaries or services to set up.

Install#

One gem, standard library only.

Gemfile
gem "stationery"

Stationery needs Ruby 3.4 or newer and has no runtime dependencies: no Prawn, no headless Chrome, no native extensions and no other processes. In a Rails app require the Railtie instead — see Rails:

gem "stationery", require: "stationery/rails"

Your first document#

A class, a view_template, to_pdf.

A document is a Stationery::Document subclass. view_template describes the page with elements; to_pdf lays them out, paginates and returns the PDF bytes.

require "stationery"

class Hello < Stationery::Document
  def view_template = text("Hello, world", size: 24, weight: :bold)
end

File.binwrite("hello.pdf", Hello.new.to_pdf)

No fonts to configure: Inter (regular, bold, italic, bold italic) ships inside the gem and is used until you declare a font_family. See Fonts for your own families.

to_pdf#

Bytes, a file, or any IO.

pdf = Hello.new.to_pdf              # => "%PDF-1.7…" (binary String)
Hello.new.to_pdf("hello.pdf")       # also writes a path…
Hello.new.to_pdf($stdout)           # …or anything that responds to #write
Hello.new.to_pdf(strict: true)      # raise instead of writing when layout warned
Hello.new.to_pdf(debug: true)       # outline every layout rectangle

After a render, document.warnings lists everything the engine noticed but did not raise on — see Warnings and strict mode.

A real document#

Page setup, fonts, a page template, a table.

class InvoicePdf < Stationery::Document
  page size: :a4, margin: [40, 44, 56, 44]
  font_family "Inter", regular: "fonts/Inter-Regular.ttf", bold: "fonts/Inter-Bold.ttf"
  default_text font: "Inter", size: 9, color: "#1F2937"
  metadata title: "Invoice"

  page_template do |page|
    box(at: [44, page.height - 34], width: page.content_box.width) do
      text "Page #{page.number} of #{page.count}", size: 7, align: :right
    end
  end

  def initialize(invoice)
    super()
    @invoice = invoice
  end

  def view_template
    row(align: :middle) do
      column(width: 0.5) { text "Invoice #{@invoice.number}", size: 22, weight: :bold }
      column(width: 0.5, align: :right) { image "logo.png", height: 34, align: :right }
    end
    rule height: 3, color: "#4F46E5"
    spacer 18
    box(background: "#F3F4F6", radius: 6, padding: 12, width: 190) do
      text "AMOUNT DUE", size: 8, weight: :bold, letter_spacing: 0.5
      text @invoice.total, size: 19, weight: :bold, color: "#4F46E5"
    end
    table(@invoice.rows, width: :full, widths: [nil, 45, 80, 85], header: true, cell: { borders: [] }) do |t|
      t.row(0).set(background: "#4F46E5", color: "#FFFFFF", weight: :bold)
      t.columns(1..).align = :right
      t.zebra(from: 2, color: "#F9FAFB")
    end
  end
end

No cursor arithmetic anywhere: padding, backgrounds, borders, radii, column widths and page breaks are the engine's job. The Cookbook walks through four complete documents.

Render from the command line#

stationery render FILE.

The gem ships a stationery executable. render loads a Ruby file, finds the Stationery::Document it defines and writes the PDF next to it:

stationery render hello.rb                  # writes hello.pdf
stationery render invoice.rb --out - > a.pdf

A document whose initialize needs arguments renders from a def self.preview class method that returns an instance built with sample data. Every option is on the CLI page.

The docs for an agent#

Over MCP, as llms.txt, and every page as Markdown.

A coding agent reads these pages as tools over the Model Context Protocol: search_docs finds sections, get_page reads a page as Markdown and list_pages names them all. In Claude Code:

claude mcp add --transport http stationery https://stationery.zoolutions.llc/mcp

Any other MCP client (Claude.ai, Cursor, Codex) takes the same URL as a remote server over HTTP:

{ "mcpServers": { "stationery": { "url": "https://stationery.zoolutions.llc/mcp" } } }

It is read-only and needs no key. Without MCP, /llms.txt is the index of the pages and /llms-full.txt all of them in one file, and each page is Markdown at its URL with .md, as /docs/elements.md. The Examples page carries the source of every example, so an agent gets them there too.

A skill that ships with the gem

The gem carries a skill for coding agents: how a document is built, the rules that surprise (a String is literal, a Float up to 1 is a share of the width, a fixed height: never splits), how to render a document to pictures and look at them before calling it done, conformance, labels and printing, and a recipe per thing people ask for (an invoice, a report, a form, a flyer, a label, a receipt), each pointing at the example that shows it. It is written from the README and the examples of the version installed, and carries that version:

stationery skill install                  # ~/.claude/skills/stationery (and Codex's, ~/.agents' when there)
stationery skill install --project        # ./.claude/skills/stationery, to commit with the app
stationery skill status                   # current, outdated or missing
stationery skill print > stationery.md    # one file, for an agent that takes one

Install it again after updating the gem; status says when it is behind. Every option is on the CLI page.

Where next#

  • Examples — twenty complete documents with previews and live PDFs.
  • Documents and components — class-level configuration and reusable pieces.
  • Elements — every element and its options.
  • Forms — fillable fields, their options and how they behave in a viewer.
  • Layout rules — how measuring, splitting and pagination decide where things land.
  • Comparison — Stationery next to Prawn and sghtmltopdf, feature by feature.