← All posts

Generating PDFs from HTML with wkhtmltopdf

March 24, 2022 rails

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_tag and wicked_pdf_image_tag. Regular Rails helpers generate URLs that wkhtmltopdf can’t resolve. The wicked_pdf helpers convert to file:// 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: always to force page breaks. Add the page-break class from the CSS above to any element that should start a new page.
  • Debug with HTML first. Comment out the format.pdf block 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-face with file:// 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.
Let’s work together! Tell Me About Your Project