Skip to content

§ Rails HTML to PDF

HTML to PDF Rails: Convert a Ruby on Rails View to a Real PDF

Turn a Rails view into a searchable, pixel accurate PDF with one call. Managed Chromium renders your real ERB or ViewComponent output with full CSS, web fonts and JavaScript, so you can leave the archived wkhtmltopdf binary behind without putting Chromium in your container. Try it on any URL or HTML below.

  • Early access, launching soon
  • No card required
  • Your HTML stays yours

§ Live demo

Runs in your browser. Nothing is uploaded.

Your PDF is downloading. Want this as one API call, with the page archived too?

§ Short answer

To convert HTML to PDF in Rails, render your view to a string with render_to_string, then hand that HTML to a renderer. The old default, WickedPDF, wraps wkhtmltopdf, which was archived in 2023 and receives no security patches, so it is not a safe choice for new apps. The current in-app options are Grover, which drives Chromium through Puppeteer and needs Node on the host, and Ferrum, which drives Chromium directly with no Node. Both mean shipping and scaling a browser yourself. The alternative is to POST the HTML to a rendering API. Sitepdf renders it in managed Chromium and returns a true, selectable vector PDF, plus an optional timestamped archive of exactly what it rendered.

Last updated July 2026. Written and fact checked by the Sitepdf team.

§ 00

Four ways to make a PDF in Rails, compared

Rails has no built-in PDF export, and the long-standing default is now on borrowed time. These four routes differ on rendering engine, what you must install and run, and whether your CSS and JavaScript survive.

Approach Engine CSS and JavaScript What you run Best for
WickedPDF
legacy
wkhtmltopdf, an old WebKit fork Dated CSS support, no modern layout; JavaScript is unreliable A vendored wkhtmltopdf binary per platform Existing apps only, as a temporary state with a migration planned
Grover
Chromium via Node
Chromium, driven through Puppeteer Full modern CSS and JavaScript, it is a real browser Node plus Puppeteer plus Chromium in your image New apps where Node on the host is already acceptable
Ferrum PDF
Chromium, no Node
Chromium, driven directly over CDP from Ruby Full modern CSS and JavaScript Chromium in your image, but no Node runtime Teams who want browser fidelity without adding Node
Managed Chromium API
Sitepdf
Chromium, run and scaled by us Full modern CSS and JavaScript One HTTP call, no browser in your container Production documents, exact fidelity, zero browser ops, a dated record

The honest read: Grover and Ferrum are good gems and give you the same Chromium output we do, for free, if you are happy to install and scale a browser. Ferrum is the lighter of the two since it drops the Node layer. An API earns its place when a browser does not belong in your container at all, when your PDF traffic is spiky and you do not want to size a browser pool for peaks, or when you need a retrievable, timestamped record of what you rendered.

§ 01

First, the WickedPDF situation

For a decade, "HTML to PDF in Rails" meant WickedPDF, and the pattern it popularized was genuinely good: you render a normal Rails view, in a normal layout, and get a PDF of it. The problem is not the gem, it is the engine underneath. WickedPDF shells out to wkhtmltopdf, and the wkhtmltopdf project was archived in 2023. It still runs, and plenty of production apps still use it, but nothing is being fixed, including security issues, and its rendering engine is an old WebKit fork that never learned modern CSS.

What that means in practice: a template written today with flexbox or CSS grid will not lay out correctly, web fonts are fragile, and JavaScript-driven content is a coin flip. If you have a WickedPDF app that works, there is no emergency, but treat it as a temporary state and pin your binary version. For anything new, start on a current engine. The wkhtmltopdf alternative page maps the replacements across every language, not just Ruby.

§ 02

Grover and Ferrum: real Chromium, in your infrastructure

The two in-app successors both give you Chromium, and the difference between them is the stack you have to carry. Grover drives Chromium through Puppeteer, which means Node and a Chromium download live in your image alongside Ruby. It keeps the familiar shape, so a WickedPDF migration is mostly mechanical: you still build HTML with render_to_string and pass it in. Ferrum talks to Chromium directly over the Chrome DevTools Protocol from Ruby, so the Node layer disappears and there is one less wrapper between your call and the browser.

Both are solid, and if you are comfortable running a browser they are the cheapest way to get browser-quality output. Be clear-eyed about what running a browser means, though. Chromium is memory hungry, a crashed or leaked browser process will take a Puma worker with it, and sizing a pool for a Monday-morning batch of statements is a real capacity exercise rather than a one-line config. Teams usually discover this the first time a month-end job renders two thousand documents at once.

§ 03

The API path: same Chromium, none of the ops

If you want browser fidelity but not a browser, the pattern stays the same and only the last step changes. Render your view to a string exactly as you would for Grover, then post it, or point the API at a live route in your app and let it load the page and run the JavaScript.

class InvoicesController < ApplicationController
  def show
    html = render_to_string(template: "invoices/show", layout: "pdf", formats: [:html])

    response = Faraday.post("https://api.sitepdf.com/v1/render") do |req|
      req.headers["Authorization"] = "Bearer #{ENV['SITEPDF_KEY']}"
      req.body = { html: html, format: "Letter", archive: "true" }
    end

    redirect_to JSON.parse(response.body)["pdf_url"], allow_other_host: true
  end
end

Your asset pipeline output, Tailwind classes, web fonts and any JavaScript charts render the way they do in Chrome, and the PDF text stays selectable and searchable rather than becoming a flat image. Nothing runs a browser inside your dyno or pod, so a spike in PDF volume does not become a memory incident in your web tier. The same call works from a Sidekiq job for background generation, which is where most Rails PDF work belongs anyway. Our HTML to PDF in Ruby guide covers the gem-by-gem detail including Prawn, and the PDF generator API page lists every parameter.

§ 04

When the document is not really HTML: Prawn

One honest caveat before you reach for any HTML renderer. If the document has no HTML version and never will, a shipping label, a boarding pass, a generated certificate, then Prawn is probably the right tool. It is pure Ruby, draws the page in code, has no browser or binary behind it, and is the fastest option because it skips a layout engine entirely. The trade is that you describe the document in Ruby rather than reusing a view, so it drifts from your web pages over time. Use Prawn when there is no page to mirror, and an HTML renderer when there is one.

§ 05

Keep a dated copy of every document you generated

A generated invoice, quote or statement leaves your Rails app and nothing verifiable stays behind. Add archive=true to the render call and Sitepdf also stores a timestamped, immutable snapshot of exactly what it rendered, retrievable by id later, so you can prove what a document said on a given date. The website archiving page explains how the record works, and if you are weighing running headless Chrome yourself, the Puppeteer alternative page runs that trade off honestly. Renders are transient by default; leave archiving off and we return the PDF and store nothing.

Post your rendered Rails view, or point the API at a live route. A real browser prints it.
curl https://api.sitepdf.com/v1/render \
  -H "Authorization: Bearer $SITEPDF_KEY" \
  -F url=https://app.example.com/invoices/8842 \
  -F format=Letter \
  -F print_background=true \
  -F archive=true

{
  "pdf_url": "https://api.sitepdf.com/v1/documents/doc_rb27f.pdf",
  "pages": 2,
  "rendered_in_ms": 1523,
  "archive": {
    "id": "arc_rb64d",
    "captured_at": "2026-07-19T21:44:52Z",
    "retrieve_url": "https://api.sitepdf.com/v1/archives/arc_rb64d"
  }
}

The API is in early access; this is the documented call shape it opens with. Full request and response walkthrough.

§ 06

Questions about this job

How do I convert HTML to PDF in Ruby on Rails?
Render your view to an HTML string with render_to_string, then pass it to a renderer. Grover and Ferrum PDF drive real Chromium inside your app, so your CSS and JavaScript survive but you install and scale a browser. A rendering API takes the same HTML over one HTTP call and returns a searchable PDF with no browser in your container.
Is WickedPDF still safe to use in 2026?
Not for new applications. WickedPDF wraps wkhtmltopdf, which was archived in 2023 and gets no further fixes, including security patches, and its old WebKit engine does not support modern CSS such as flexbox or grid. Existing apps can keep running short term with a pinned binary version, but plan a migration to Grover, Ferrum or a hosted API.
Grover or Ferrum, which should I use in Rails?
Both render with real Chromium and produce equivalent output. Grover goes through Puppeteer, so Node and Chromium both live in your image, and its API is closest to WickedPDF, which makes migration easier. Ferrum talks to Chromium directly over CDP from Ruby, so you drop the Node runtime and one wrapper layer.
How do I generate a PDF in Rails without installing Chromium?
Send your rendered view HTML to a hosted rendering API. The browser runs on their infrastructure, so your image stays Ruby-only, your dynos never carry a memory-hungry browser process, and a spike in PDF volume does not need a browser pool sized for peak. You get back a true vector PDF with selectable text in one call.
Should I use Prawn or an HTML to PDF renderer in Rails?
Use Prawn when the document has no HTML version and never will, such as a label or certificate, since it is pure Ruby, needs no browser and is the fastest option. Use an HTML renderer when the PDF should mirror a page your users already see, so you reuse the view instead of maintaining a separate layout in Ruby code.

§ 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