How to Convert Raw HTML to PDF in Ruby
Convert an HTML string to a reliable PDF in Ruby with Grover, PDFKit, or Wicked PDF, including assets, CSS, options, errors, and production guidance.

Direct answer: If your Ruby application already has an HTML string, use Grover when you want Chromium and Puppeteer rendering, or PDFKit when wkhtmltopdf fits your runtime. Both accept HTML strings and return PDF bytes. For Rails applications that need view integration, Wicked PDF is another wkhtmltopdf wrapper. The most common conversion failure is missing CSS, images, or fonts because relative URLs have no usable base; set Grover’s display_url, configure PDFKit’s root_url/protocol, or rewrite references to absolute URLs.
This guide shows complete Ruby implementations, renderer choices, asset handling, print CSS, page options, production hardening, troubleshooting, and an API alternative when you do not want to operate a browser locally.
1. Choose the renderer
| Option | Engine | Best fit | Deployment requirement |
|---|---|---|---|
| Grover | Puppeteer and Chromium | Modern CSS, JavaScript, browser-faithful output | Chromium and the documented Puppeteer dependency |
| PDFKit | wkhtmltopdf | Existing wkhtmltopdf workflows and simple HTML | Manually installed, discoverable wkhtmltopdf executable |
| Wicked PDF | wkhtmltopdf | Rails views and controller responses | wkhtmltopdf plus Rails integration |
Grover’s current RubyGems listing states Ruby >= 3.0.0 and < 3.5.0 for the listed release; verify the version you install against your deployment. PDFKit’s README says automated wkhtmltopdf installation was removed, so install the executable through your operating system or image build. Renderer versions and supported flags can change, so check the README and installed binary before relying on a particular option.
2. Convert an HTML string with Grover
Grover passes your HTML to Chromium through Puppeteer. Install the gem and its documented Puppeteer dependency, then call to_pdf.

# Gemfile
gem 'grover'
bundle install
require 'grover'
html = <<~HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
@page { size: A4; margin: 18mm 15mm; }
body { font-family: Arial, sans-serif; color: #222; }
h1 { margin-top: 0; }
</style>
</head>
<body>
<h1>Invoice 1042</h1>
<p>Generated from an HTML string in Ruby.</p>
</body>
</html>
HTML
pdf = Grover.new(
html,
display_url: 'https://example.com/'
).to_pdf
File.binwrite('invoice.pdf', pdf)
The display_url gives Chromium a base for relative references such as /stylesheets/invoice.css and images/logo.svg. Grover documents a fallback display URL of http://example.com; do not depend on that fallback for application assets. Either set a URL that serves those paths or preprocess the HTML so every asset URL is absolute.
Grover options you will commonly need
pdf = Grover.new(
html,
display_url: 'https://app.example.test/',
format: 'A4',
landscape: false,
margin: {
top: '18mm',
right: '15mm',
bottom: '18mm',
left: '15mm'
},
print_background: true,
prefer_css_page_size: true,
page_ranges: '1-3',
display_header_footer: false
).to_pdf
Pass only options supported by the Grover version and underlying Puppeteer version you deploy. Renderer options cover paper format, custom width and height, margins, page ranges, background graphics, headers and footers, and CSS page-size preference. For a long document, use CSS page-break rules such as break-before, break-after, and break-inside: avoid.
3. Control CSS media and page layout
Puppeteer’s page.pdf() uses the print CSS media type by default, as documented in the Puppeteer API. Put PDF-specific rules inside @media print or use @page for paper size and margins. If your design only works with screen styles, configure the wrapper to emulate screen media before PDF generation; the exact option name depends on the Grover release, so confirm it in that version’s README.
<style>
@page { size: Letter; margin: 0.65in; }
@media print {
.screen-only { display: none !important; }
.invoice { break-inside: avoid; }
a { color: #111; text-decoration: none; }
}
</style>
Use embedded fonts or absolute font URLs that the renderer can reach. A web page that looks correct in your browser can produce fallback fonts in a container with no network access. Wait for web fonts and images to finish loading before generating the PDF when your wrapper exposes a wait or page hook.
4. Convert with PDFKit and wkhtmltopdf
PDFKit wraps the wkhtmltopdf executable. Its README documents both returning PDF bytes and writing directly to a file.
# Gemfile
gem 'pdfkit'
bundle install
require 'pdfkit'
html = <<~HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>body { font-family: sans-serif; }</style>
</head>
<body>
<h1>Report</h1>
<p>Converted by wkhtmltopdf through PDFKit.</p>
</body>
</html>
HTML
kit = PDFKit.new(
html,
page_size: 'A4',
margin_top: '18mm',
margin_right: '15mm',
margin_bottom: '18mm',
margin_left: '15mm',
print_media_type: true
)
File.binwrite('report.pdf', kit.to_pdf)
# Or: kit.to_file('report.pdf')
For raw HTML, PDFKit needs complete resource paths or a configured base. Set root_url and protocol when your HTML uses root-relative links, or rewrite links to absolute URLs before creating the kit. Ensure the wkhtmltopdf executable is on PATH, or configure PDFKit with its explicit path according to the installed version.
5. Rails integration with Wicked PDF
Wicked PDF is useful when the source is a Rails view rather than a manually assembled string. Its documented flow stages HTML and assets before invoking wkhtmltopdf.
# Gemfile
gem 'wicked_pdf'
# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
def show
respond_to do |format|
format.html
format.pdf do
render pdf: 'report',
template: 'reports/show',
page_size: 'A4',
margin: { top: 18, bottom: 18, left: 15, right: 15 }
end
end
end
end
Use the Rails asset helpers and the integration instructions in the Wicked PDF README. The project lists verified Ruby 2.2 through 3.2 and Rails 4 through 7.0; treat those as the project’s documented compatibility claims and verify your current Rails, Ruby, and wkhtmltopdf combination.
6. Make assets, data, and JavaScript reliable
- Resolve every URL. Convert relative CSS, image, script, and font paths to absolute HTTPS URLs, or provide the renderer a base URL.
- Authenticate private assets. Pass cookies or headers through the renderer where supported, or serve a short-lived signed asset URL. Do not put long-lived secrets in HTML.
- Wait for dynamic content. Server-render the final values when possible. Otherwise wait for a selector, a known delay, or network idle before producing the PDF.
- Inline critical CSS. This reduces failures caused by blocked networks and makes the HTML portable.
- Use deterministic dates and locale. Pass the report date and timezone as data rather than relying on the renderer host.
- Limit untrusted HTML. Sanitize user input and isolate rendering jobs. HTML-to-PDF engines execute or fetch content according to their configuration.
7. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| PDF is blank | HTML is empty, navigation failed, or content is rendered after capture | Log the final HTML, verify the base URL, and wait for the content selector or data request. |
| Images or CSS are missing | Relative URLs have no base, or the renderer cannot reach the host | Set display_url, configure root_url/protocol, or use absolute URLs; check container DNS and TLS. |
| Fonts differ from the browser | Font files are unavailable or loading was not finished | Embed or expose fonts, allow their origin, and wait for document.fonts.ready when your integration permits it. |
| Modern CSS is ignored | wkhtmltopdf’s rendering engine does not support the feature | Use Grover/Chromium for browser-level CSS, or simplify the stylesheet. |
| Command not found | wkhtmltopdf is not installed or absent from PATH |
Install it in the image and configure PDFKit/Wicked PDF with the executable path. |
| Process times out | Slow external resources, an infinite script, or too many concurrent browsers | Set a bounded timeout, remove unnecessary third-party requests, add retries for transient failures, and cap concurrency. |
| Content is cut between pages | Uncontrolled page breaks or oversized elements | Use break-inside: avoid, explicit page breaks, smaller media, and sensible margins. |
| Works locally but fails in production | Different Ruby, browser, binary, fonts, network policy, or locale | Pin compatible versions, build the renderer into the deployment image, and log versions at startup. |
8. Performance, reliability, and cost
Launching Chromium or wkhtmltopdf is substantially heavier than string processing. Reuse a controlled browser process where your integration supports it, limit simultaneous jobs, and queue large documents. Keep HTML and assets close to the renderer, avoid analytics and advertisements, and cache identical output using a content hash of the HTML, options, and data version.
Set hard limits for input size, page count, navigation time, and total job duration. Retry only transient failures; retrying malformed HTML or an unavailable asset indefinitely increases load. Record renderer version, option values, elapsed time, output size, and the first failing resource so a bad PDF is diagnosable.
Cost includes CPU and memory for the renderer, container image size, dependency maintenance, and engineering time for browser compatibility. wkhtmltopdf can be simpler to operate where its CSS model is sufficient; Chromium is often the safer choice for modern layouts and JavaScript-heavy pages. Validate representative invoices, tables, long paragraphs, images, RTL text, and page breaks in the exact production image.
9. Or skip the browser setup
ScreenshotNeo can return a PDF from a URL through one request, so your Ruby service does not need to package Chromium or wkhtmltopdf. Cookie and consent banners, newsletter popups, and chat widgets are removed before the capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools.

See the ScreenshotNeo API documentation for authentication and PDF options.
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', body);
ScreenshotNeo includes PDF paper size, margins, landscape mode, page ranges, custom headers, cookies, authorization, waiting rules, JavaScript, custom CSS, caching, asynchronous jobs, signed webhooks, bulk capture, and a usage API. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
10. FAQ
Can I convert HTML without writing it to disk?
Yes. Grover and PDFKit accept an HTML string and return PDF bytes, which you can stream from a controller or store with File.binwrite.
Which option handles modern CSS best?
Grover uses Chromium through Puppeteer. PDFKit and Wicked PDF use wkhtmltopdf, so compare the CSS features your document actually uses.
Why do relative image paths fail?
A raw string has no application origin. Supply a display or root URL, or rewrite every reference to an absolute URL.
Can I use screen styles in a PDF?
Chromium PDF generation defaults to print media. Add print rules, or configure media emulation in the wrapper version you use.
Should I render untrusted user HTML?
Sanitize it, restrict network access, and isolate jobs. A renderer may process scripts and remote resources depending on its configuration.


