Skip to content

HTML to PDF in Ruby on Rails: your real options in 2026

The default Rails answer, WickedPDF over wkhtmltopdf, is now a liability: the binary was archived in 2023 with unpatched CVEs and CSS frozen at 2014. Here are the paths that actually work in 2026, Grover, Ferrum, Prawn and a hosted API, with code and when each fits.

For years the answer to "how do I make a PDF in Rails" was one word: WickedPDF. It was the gem everyone reached for, and it worked. In 2026 it is a liability, because the binary it depends on has been abandoned. That has quietly reshuffled the whole landscape, so if you are starting fresh or maintaining an app that still leans on the old default, here is the current map of Rails PDF generation: what to avoid, what actually renders like a browser, and where a hosted API fits.

The short version

Do not build new PDF features on WickedPDF or PDFKit, because both wrap wkhtmltopdf, which was archived in 2023 with unpatched CVEs and CSS support frozen around 2014. For output that matches a real browser, use Grover, which drives headless Chromium through Puppeteer, or Ferrum PDF, which talks to Chromium directly from pure Ruby with no Node. If you have no HTML at all and just need to lay out a structured document in code, Prawn is the fastest option. And when you would rather not run a browser in your container at all, call a hosted rendering API. Match the tool to how faithful the output must be and whether Chromium belongs in your infrastructure.

Why WickedPDF is now a dead end

WickedPDF itself is fine Ruby code; the problem is underneath it. WickedPDF and PDFKit are both wrappers around the wkhtmltopdf binary, and wkhtmltopdf was formally archived by its maintainer in early 2023. That means no more releases, several known CVEs that will never be patched, and a rendering engine based on a years old fork of WebKit whose CSS support effectively stopped advancing around 2014. Flexbox is unreliable, CSS grid is absent, modern web fonts are hit or miss, and anything driven by JavaScript simply does not run.

If you have a legacy app that already generates PDFs with WickedPDF and they look correct, there is no emergency to rip it out this week. But do not write new features against it, and plan a migration, because you are shipping an unpatched binary with a frozen engine. The full case, and the safe replacements, is in the wkhtmltopdf alternative guide. Everything below is a way off that binary.

Grover: real Chromium, the closest WickedPDF replacement

Grover is the most direct upgrade. It is a Ruby wrapper around Puppeteer, the Node library that drives headless Chromium, so you pass an HTML string and get PDF bytes back, exactly the shape WickedPDF gave you. The difference is that a real, current browser does the rendering, so flexbox, grid, web fonts and JavaScript all work and the PDF matches what a user sees in Chrome.

# Gemfile: gem 'grover'  (also needs Node + puppeteer installed)
html = ApplicationController.render(
  template: 'invoices/show',
  layout: 'pdf',
  assigns: { invoice: invoice }
)
pdf = Grover.new(html, format: 'Letter', print_background: true).to_pdf
send_data pdf, filename: "invoice-#{invoice.id}.pdf"

The cost is in that comment. Grover needs Node and a Chromium install alongside your Ruby, so your Docker image grows and your deploy now manages a browser. Set print_background: true or Chromium strips background colors and images in print mode, which is the single most common reason a Grover PDF comes out looking washed out next to the screen. Grover is an excellent choice when Node and Chromium are welcome in your environment; it just does not remove the browser, it embraces it.

Ferrum PDF: Chromium output without Node

Ferrum PDF is the same idea with one big difference: it is pure Ruby. Instead of shelling out to Puppeteer, it speaks the Chrome DevTools Protocol directly to a Chromium binary, so you still get real browser rendering but you drop the entire Node dependency. For a Ruby team that does not want a JavaScript runtime in the image just to print PDFs, that is a meaningful simplification. You still install and run Chromium, so the memory and concurrency realities of a browser are still yours to manage, but the toolchain is smaller and the moving parts are fewer. If you are choosing between the two, the rule of thumb is simple: Grover when Node is already fine on the host, Ferrum PDF when you want to keep Node out.

Prawn: for documents that are not HTML

Prawn is the odd one out, and that is the point. It does not render HTML at all; it is a pure Ruby library for drawing a PDF programmatically, positioning text, tables, images and shapes yourself. Because it skips a layout engine entirely, it is the fastest in process option and it has no external dependencies, no binary and no browser. The tradeoff is that you build the document in Ruby code rather than in a template, which is painful for anything visually rich but perfectly reasonable for a highly structured, stable document like a shipping label, a boarding pass or a fixed format statement. If your data lives in your database and you want to assemble a report from it, you can even query it in plain English to shape the dataset first, then hand the rows to Prawn. Reach for Prawn when there is no HTML and speed matters; reach for anything else when you have a template.

A hosted API: keep the browser out of your app

Grover and Ferrum both give you browser fidelity, and both put a browser in your infrastructure. A hosted rendering API is the option that gives you the fidelity without the browser: managed Chromium runs on someone else's servers, and from Rails it is a single HTTP request. No Node, no Chromium in your image, no memory pool to tune, and it works on platforms where you cannot run a browser at all, like many managed and serverless hosts. The HTML to PDF in Rails page compares all four routes side by side with the Rails controller code for each.

require 'net/http'
uri = URI('https://api.sitepdf.com/v1/render')
res = Net::HTTP.post_form(uri, {
  'url'     => invoice_url(invoice),
  'format'  => 'Letter',
  'archive' => 'true'          # keep a timestamped copy of this render
})
pdf_url = JSON.parse(res.body)['pdf_url']

That archive flag is the capability no gem offers. It stores a timestamped snapshot of exactly what was rendered, retrievable later by id, so you can prove months from now what a given invoice or statement looked like when you produced it. For finance and compliance heavy Rails apps that provenance is frequently the reason to use an API over a local library. The PDF generator API page has the full call shape, and the Gotenberg alternative page covers the self host versus hosted decision if you are weighing running your own container instead.

Which should you pick

SituationBest fit
New feature, output must match Chrome, Node is fine on the hostGrover
Same, but you want to drop the Node dependencyFerrum PDF
No HTML, structured document, speed mattersPrawn
Browser fidelity with no browser in your image, plus a dated archiveHosted rendering API
Legacy app on WickedPDF that already worksLeave it, but plan the migration

The one answer that is no longer on the table for new code is WickedPDF, and by extension any path that ends at wkhtmltopdf. Everything else comes down to a single question: do you want a browser in your infrastructure? If yes, Grover or Ferrum give you real Chromium output that you run. If no, a hosted API gives you the same output without it. Prawn sits off to the side for the cases where there is no HTML to render in the first place. Decide the browser question and the rest of the choice makes itself, and if your answer is a firm no, HTML to PDF without headless Chrome covers the two ways to avoid one and what each costs you in CSS fidelity.

Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API