Generating PDFs from HTML with wkhtmltopdf
Building a custom PDF layout from scratch with libraries like Prawn is tedious. You already have HTML templates — why not render those as PDFs? wkhtmltopdf is a command-line tool that uses a WebKit rendering engine to convert HTML to PDF. Combined with the wicked_pdf gem, you can render any Rails view as a downloadable PDF.
Install
Install wkhtmltopdf on your system:
# macOS
brew install wkhtmltopdf
# Ubuntu/Debian
sudo apt install wkhtmltopdf
Add the gems:
# Gemfile
gem "wicked_pdf"
gem "wkhtmltopdf-binary" # bundles the binary for Heroku/CI
bundle install
rails generate wicked_pdf
The generator creates config/initializers/wicked_pdf.rb. The defaults are usually fine.
Register the PDF MIME Type
config/initializers/mime_types.rb:
Mime::Type.register "application/pdf", :pdf
A Basic PDF Response
class InvoicesController < ApplicationController
def show
@invoice = Invoice.find(params[:id])
respond_to do |format|
format.html
format.pdf do
render pdf: "invoice_#{@invoice.number}",
template: "invoices/show",
layout: "pdf",
disposition: "inline"
end
end
end
end
Visit /invoices/123.pdf and you get a PDF. The same controller action serves both HTML and PDF — the respond_to block picks the format based on the URL extension.
Options:
pdf:— the filename (without.pdf)template:— which view to render (defaults to the current action)layout:— a separate layout for PDFs (more on this below)disposition:—"inline"displays in the browser,"attachment"triggers a download
The PDF Layout
PDFs need a different layout than your web pages — no navbar, no JavaScript, different CSS. Create a dedicated layout:
app/views/layouts/pdf.html.erb:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<%= wicked_pdf_stylesheet_link_tag "pdf" %>
</head>
<body>
<%= yield %>
</body>
</html>
wicked_pdf_stylesheet_link_tag inlines the CSS as a file:// URL that wkhtmltopdf can read. Regular stylesheet_link_tag won’t work because wkhtmltopdf can’t access the asset pipeline’s HTTP URLs during rendering.
PDF Styles
app/assets/stylesheets/pdf.css:
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 12px;
color: #333;
line-height: 1.5;
}
h1 { font-size: 24px; margin-bottom: 10px; }
h2 { font-size: 18px; margin-bottom: 8px; }
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
table th, table td {
padding: 8px 12px;
border-bottom: 1px solid #ddd;
text-align: left;
}
table th {
font-weight: 600;
border-bottom: 2px solid #333;
}
.text-right { text-align: right; }
.text-muted { color: #999; }
.mt-4 { margin-top: 40px; }
.mb-2 { margin-bottom: 20px; }
.page-break { page-break-after: always; }
Keep PDF styles simple and self-contained. CSS frameworks like Bootstrap work but add a lot of unused weight. A small dedicated stylesheet is easier to debug.
An Invoice Template
app/views/invoices/show.html.erb:
<div style="display: flex; justify-content: space-between; margin-bottom: 40px;">
<div>
<h1>Invoice</h1>
<p class="text-muted">#<%= @invoice.number %></p>
</div>
<div style="text-align: right;">
<strong>Your Company Name</strong><br>
123 Main Street<br>
Minneapolis, MN 55401
</div>
</div>
<div class="mb-2">
<strong>Bill To:</strong><br>
<%= @invoice.client.name %><br>
<%= @invoice.client.address %>
</div>
<div class="mb-2">
<strong>Date:</strong> <%= @invoice.date.strftime("%B %-d, %Y") %><br>
<strong>Due:</strong> <%= @invoice.due_date.strftime("%B %-d, %Y") %>
</div>
<table>
<thead>
<tr>
<th>Description</th>
<th class="text-right">Qty</th>
<th class="text-right">Rate</th>
<th class="text-right">Amount</th>
</tr>
</thead>
<tbody>
<% @invoice.line_items.each do |item| %>
<tr>
<td><%= item.description %></td>
<td class="text-right"><%= item.quantity %></td>
<td class="text-right"><%= number_to_currency item.rate %></td>
<td class="text-right"><%= number_to_currency item.amount %></td>
</tr>
<% end %>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="text-right"><strong>Total</strong></td>
<td class="text-right"><strong><%= number_to_currency @invoice.total %></strong></td>
</tr>
</tfoot>
</table>
This template works for both HTML and PDF. The browser shows it with your app layout; the PDF renderer uses the pdf layout.
Headers and Footers
wkhtmltopdf supports separate HTML files for headers and footers on every page:
render pdf: "invoice_#{@invoice.number}",
template: "invoices/show",
layout: "pdf",
header: { html: { template: "shared/pdf_header" } },
footer: { html: { template: "shared/pdf_footer" } },
margin: { top: 30, bottom: 25 }
app/views/shared/pdf_footer.html.erb:
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Helvetica, sans-serif; font-size: 9px; color: #999; }
.footer { text-align: center; padding: 10px 20px; }
</style>
</head>
<body>
<div class="footer">
Page <span class="page"></span> of <span class="topage"></span>
</div>
</body>
</html>
wkhtmltopdf replaces .page and .topage with actual page numbers automatically. Header and footer templates are standalone HTML documents — they don’t share styles with the main content.
Increase the top and bottom margins to make room for the header and footer content.
Page Size and Orientation
render pdf: "report",
template: "reports/show",
layout: "pdf",
page_size: "Letter", # or "A4" (default)
orientation: "Landscape", # or "Portrait" (default)
margin: { top: 15, bottom: 15, left: 10, right: 10 }
Sending as an Email Attachment
Generate the PDF in memory and attach it:
class InvoiceMailer < ApplicationMailer
def send_invoice(invoice)
@invoice = invoice
pdf = WickedPdf.new.pdf_from_string(
render_to_string(
template: "invoices/show",
layout: "pdf"
)
)
attachments["invoice_#{invoice.number}.pdf"] = pdf
mail(to: invoice.client.email, subject: "Invoice ##{invoice.number}")
end
end
pdf_from_string takes rendered HTML and returns raw PDF bytes. Attach those bytes directly — no temp file needed.
Tips
- Use
wicked_pdf_stylesheet_link_tagandwicked_pdf_image_tag. Regular Rails helpers generate URLs that wkhtmltopdf can’t resolve. The wicked_pdf helpers convert tofile://paths. - Avoid Flexbox and Grid for critical layout. wkhtmltopdf’s WebKit engine is old. Flexbox works for simple cases but complex layouts can break. Tables are more reliable for PDF layout.
- Use
page-break-after: alwaysto force page breaks. Add thepage-breakclass from the CSS above to any element that should start a new page. - Debug with HTML first. Comment out the
format.pdfblock temporarily and render the PDF template as regular HTML. Fix layout issues in the browser where debugging is easier, then switch back to PDF. - Fonts. wkhtmltopdf uses system fonts. If you need custom fonts, either install them on the server or use
@font-facewithfile://paths to font files in your assets directory. - Performance. wkhtmltopdf spawns a headless browser process for each PDF. For high-volume generation, consider queuing PDFs in a background job.