Convert an HTML Invoice to PDF in Ruby on Rails
Render a Rails invoice view to PDF with Grover or Wicked PDF, return it as a download, and handle assets, deployment, security, and troubleshooting.
To convert an HTML invoice to PDF in Ruby on Rails, render an invoice template to HTML, pass that HTML to a PDF renderer, and send the resulting bytes with the PDF content type and a download filename. Two common Rails paths are Grover, which uses Puppeteer and Chromium, and Wicked PDF, which invokes wkhtmltopdf. Choose based on the renderer your invoice needs and the browser runtime your deployment can support. Check the resulting PDF in the production environment: matching an ordinary browser page is not automatic.
1. Choose a renderer
| Option | Rendering engine | Consider it when | Deployment detail |
|---|---|---|---|
| Grover | Puppeteer and Chromium | Your invoice relies on modern browser CSS or JavaScript. | Install and package the gem, Puppeteer, and a compatible Chromium runtime. |
| Wicked PDF | wkhtmltopdf | You want its Rails controller integration and your invoice works with that renderer. | Install the wkhtmltopdf executable where the Rails process can run it. |
| PDFKit | wkhtmltopdf | You want a Ruby wrapper or Rails/Rack middleware around wkhtmltopdf. | Install and verify the binary and the exact gem release’s compatibility. |
These tools use different rendering runtimes. Test your real invoice styles, scripts, fonts, and assets with the chosen engine rather than assuming browser output will match. The project documentation lists version ranges for particular releases; verify compatibility for the versions you install. Wicked PDF documentation lists Ruby 2.2–3.2 and Rails 4–7.0 as verified versions; PDFKit documentation lists Ruby 2.5–3.1 and selected Rails versions through 7.0. These are documentation statements, not guarantees for other releases.
2. Prepare an invoice template
Keep invoice rendering in a dedicated template and provide it with the invoice record and any related data it needs. A dedicated layout avoids application navigation, buttons, and other page elements appearing in the document.
<!-- app/views/invoices/pdf.html.erb -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice <%= @invoice.number %></title>
<style>
@page { size: A4; margin: 18mm; }
body { font: 12px Arial, sans-serif; color: #222; }
h1 { margin: 0 0 16px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border-bottom: 1px solid #ddd; text-align: left; }
.total { text-align: right; font-weight: bold; margin-top: 16px; }
thead { display: table-header-group; }
tr { break-inside: avoid; }
</style>
</head>
<body>
<h1>Invoice <%= @invoice.number %></h1>
<p>Issued: <%= l(@invoice.issued_at.to_date) %></p>
<p>Bill to: <%= @invoice.customer.name %></p>
<table>
<thead><tr><th>Description</th><th>Qty</th><th>Amount</th></tr></thead>
<tbody>
<% @invoice.line_items.each do |item| %>
<tr>
<td><%= item.description %></td>
<td><%= item.quantity %></td>
<td><%= number_to_currency(item.amount) %></td>
</tr>
<% end %>
</tbody>
</table>
<p class="total">Total: <%= number_to_currency(@invoice.total) %></p>
</body>
</html>
Adapt the fields and currency formatting to your application. Use Rails’ escaped ERB output for customer-controlled values; avoid marking untrusted invoice descriptions as HTML-safe. Put print-specific page size, margins, and page-break rules in the template or its print stylesheet.
3. Generate and download the PDF with Grover
Grover’s documented Rails pattern renders a template with render_to_string and passes the HTML to Grover. Install the grover gem and Puppeteer package as described in its project documentation. The documentation also explains that relative asset paths need a suitable display_url or must be rewritten as absolute paths.
# Gemfile
gem "grover"
# Install the Puppeteer package using the package manager used by your app.
# See https://github.com/studiosity/grover for the documented setup.
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def show
@invoice = current_account.invoices.includes(:customer, :line_items).find(params[:id])
end
def pdf
@invoice = current_account.invoices.includes(:customer, :line_items).find(params[:id])
html = render_to_string(template: "invoices/pdf", layout: false)
pdf = Grover.new(html, display_url: request.base_url).to_pdf
send_data pdf,
filename: "invoice-#{@invoice.number}.pdf",
type: "application/pdf",
disposition: "attachment"
end
end
# config/routes.rb
resources :invoices, only: [:show] do
member do
get :pdf
end
end
The example scopes invoice lookup to the current account; use your application’s authorization and tenant-scoping rules. request.base_url supplies a base for relative URLs, but it does not by itself make every asset reachable. Ensure the renderer can access each stylesheet, image, and font in the environment where the PDF is generated.
4. Generate and download the PDF with Wicked PDF
Wicked PDF wraps the wkhtmltopdf shell utility. Install both the gem and a compatible binary. Its README shows a Rails respond_to PDF response and supports rendering or saving PDF bytes. Since wkhtmltopdf runs outside the Rails process, provide resolvable asset URLs; its documentation advises absolute references for CSS, JavaScript, and images. See the Wicked PDF README for installation and configuration details.
# Gemfile
gem "wicked_pdf"
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def pdf
@invoice = current_account.invoices.includes(:customer, :line_items).find(params[:id])
respond_to do |format|
format.pdf do
render pdf: "invoice-#{@invoice.number}",
template: "invoices/pdf",
layout: false,
disposition: "attachment"
end
end
end
end
# config/routes.rb
resources :invoices, only: [:show] do
member do
get :pdf
end
end
Confirm the installed gem version’s options and executable discovery in its README. If you need raw bytes for storage or another response flow, Wicked PDF documents rendering to a string or saving the generated result. Set and verify asset host or absolute URLs as appropriate for your setup.
5. Make assets and print layout render correctly
- CSS: Prefer a print stylesheet or inline invoice styles when that makes the rendering environment simpler. Confirm the renderer can load external stylesheets.
- Images and logos: Use absolute URLs or paths the renderer can resolve. Verify access from the production process, including any authentication or network restrictions.
- Fonts: Ensure font files are accessible and wait for font loading when using browser automation. A fallback font can change line wrapping and pagination.
- Relative paths: Grover documents using
display_urlor absolute paths for HTML rendered directly from a string. wkhtmltopdf likewise needs references it can resolve outside Rails. - Print CSS: Define page size and margins with
@page; check repeated table headers and whether long rows split acceptably. Renderer support can differ. - JavaScript: Keep invoice-critical data in server-rendered HTML where possible. If rendering depends on client-side scripts, verify script execution and readiness in the selected engine.
Puppeteer’s Page.pdf() uses print media by default. If you need screen styles instead, its API documentation says to emulate screen media before calling PDF generation. See the Puppeteer Page.pdf API documentation.
6. Validate the output before shipping
- Generate PDFs for a short invoice, a multi-page invoice, and one with unusually long descriptions.
- Check totals, tax, invoice number, dates, Unicode names, and currency symbols against the source record.
- Inspect page breaks, repeated headers, margins, logo sizing, and footer placement.
- Test with the exact production runtime, binary, fonts, and asset access configuration.
- Confirm the response has
Content-Type: application/pdf, the intended filename, and the download disposition. - Check that an unauthorized user cannot retrieve another account’s invoice by changing the record ID.
7. Troubleshooting
| Symptom | Likely cause | What to check |
|---|---|---|
| Missing logo, CSS, or images | The renderer cannot resolve a relative path or reach the asset URL. | Use absolute references or the renderer’s base URL setting; verify the production process can access the resource. |
| Executable or browser not found | The wkhtmltopdf binary or Puppeteer/Chromium runtime is missing or not available in the deployment. | Install the runtime in the deployed image and follow the selected gem’s documented setup. |
| PDF styling differs from the browser | Different rendering engine, print media rules, unsupported CSS, or fonts not loaded. | Inspect print styles and test with the production renderer; use styles supported by that engine. |
| Blank or partially rendered PDF | HTML depends on scripts or remote resources that are unavailable or unfinished. | Prefer server-rendered invoice data; check resource access and renderer readiness/configuration. |
| Unexpected page breaks | Long rows or content exceed the available page area. | Test long descriptions, adjust print CSS, and decide how rows and totals should split. |
| Request hangs or times out | Renderer startup, slow assets, or expensive document rendering exceeds the request budget. | Reduce dependencies, check renderer logs and asset response times, and consider moving generation to a background job for workloads that should not hold an HTTP request open. |
| Internal host or metadata access risk | Untrusted HTML or remote resources can cause the renderer to request internal destinations. | Do not render unsanitized user-provided HTML; restrict renderer network access and validate allowed resource destinations. Wicked PDF documents this server-side request risk in its security guidance. |
8. Performance, reliability, and cost
The research documentation provides no controlled performance comparison between Grover and wkhtmltopdf, so choose by runtime fit and measured behavior in your own deployment. Both approaches add a rendering runtime beyond ordinary template rendering. Account for its installation, startup, memory use, asset loading, and failure handling in your app’s operational design.
For low-volume downloads, generating during the request is straightforward. If generation can take long enough to affect request handling, generate asynchronously, store the PDF, and let the user retrieve it when ready. Cache only when the invoice version and permissions are accounted for; regenerate when invoice data changes. Treat generated documents as sensitive files and apply the same access rules as the underlying invoice.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A one-call capture can help create an invoice visual preview; ScreenshotNeo can also return PDFs, with the PDF settings in its API documentation. This is a hosted capture option when you do not want to package a browser runtime yourself.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for AI agents. - The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Can I return a PDF from an existing invoice show page?
Yes. Add a PDF format or dedicated PDF action, render the invoice template, and return the bytes with a PDF content type and attachment filename. Keep the invoice lookup within your authorization scope.
Should I use Chromium or wkhtmltopdf?
Choose based on the invoice’s CSS and JavaScript needs and the runtime you can deploy. Verify the output with representative invoices; the supplied project documentation does not establish a universal winner.
Can I use PDFKit instead?
Yes. PDFKit is another Ruby wrapper around wkhtmltopdf and documents Rails/Rack middleware use. Install the binary and check compatibility for the exact release you choose.
Does the generated PDF automatically use the browser’s screen styling?
Not necessarily. Puppeteer PDF generation uses print media by default; consult its API documentation if you need screen media behavior.


