Render the Rails view into an HTML string, load that string into a Puppeteer page, call page.pdf(), and return the resulting bytes from Rails with send_data. Use render_to_string in a controller action or ApplicationController.renderer.render outside one. The main details to get right are the rendering context for asset URLs, page readiness, print CSS, and how the PDF bytes are delivered.
The Rails-to-Puppeteer sequence
Rails and Puppeteer run in separate processes in the common architecture: Rails renders a view; a Node.js process receives the markup and uses Chromium through Puppeteer; the PDF bytes travel back to Rails for the HTTP response. The snippets below show that boundary explicitly. They assemble the documented Rails and Puppeteer APIs into an implementation pattern; they are not a claim that the two projects publish or test one combined integration.
- Render a PDF-specific Rails template to a string.
- Send that HTML string to a Node service that runs Puppeteer.
- Set content and wait for the resources your template needs.
- Call
page.pdf()and return its bytes. - Use Rails
send_datawith an application/pdf content type and a filename.
Rails documents rendering views to strings; Puppeteer documents Page.pdf() as returning a Promise<Uint8Array>. Verify the APIs and runtime behavior against the Rails and Puppeteer versions you deploy.
Render the HTML in Rails
Inside a controller action
When the request is already being handled by a controller, render_to_string uses the same options as render but gives you the rendered output rather than immediately sending it as the response. Keep the document in a template and pass the data it needs explicitly.
#1 Best Overall
- INNOVATIVE CARTRIDGE-FREE PRINTING — No more dealing with lots of tiny ink cartridges; With this wireless document and photo printer each ink bottle set is equivalent to about 90 individual cartridges²
- LESS FREQUENT INK REPLACEMENT — Replacement ink bottles don't have to be changed nearly as often as ink cartridges¹; When you choose this combination printer, scanner and copier you can print up to 4,500 pages black/7,500 color³
- COLOR PRINTING — Up to 2 years of ink in the box4 (and with every replacement ink set) for fewer out-of-ink frustrations
- ZERO CARTRIDGE WASTE — By using an Epson EcoTank printer you can help reduce the amount of cartridge waste ending up in landfills
- HOME PRINTER DESIGNED FOR RELIABILITY — The Epson EcoTank ET-2800 All-in-One Supertank Color Printer creates vivid, detailed prints and documents thanks to Micro Piezo Heat-Free Technology; Fire off 10 ISO pages per minute1 to easily finish large jobs
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def pdf
invoice = Invoice.find(params[:id])
html = render_to_string(
template: "invoices/pdf",
layout: "pdf",
formats: [:html],
locals: { invoice: invoice }
)
pdf_bytes = PdfRenderer.new.render(html)
send_data pdf_bytes,
filename: "invoice-#{invoice.id}.pdf",
type: "application/pdf",
disposition: "attachment"
end
end
Create matching templates such as app/views/invoices/pdf.html.erb and, if used, app/views/layouts/pdf.html.erb. A PDF layout can omit navigation, buttons, and other interactive elements that belong on the normal web page. Rails also supports rendering without a layout; choose the option deliberately so you do not accidentally include the ordinary site shell.
Outside a controller action
Jobs, services, and other code outside a controller can use ApplicationController.renderer.render. Its renderer supplies a mocked Rack request. Set the host and HTTPS context when templates generate absolute route or asset URLs; otherwise links may point to an unsuitable host or protocol. Rails documents renderer environment options in the ActionController::Renderer API.
renderer = ApplicationController.renderer.new(
http_host: "app.example.com",
https: true
)
html = renderer.render(
template: "invoices/pdf",
layout: "pdf",
formats: [:html],
locals: { invoice: invoice }
)
Use the real host and protocol appropriate to your deployment, not a value supplied unchecked by a client. If the view only uses relative URLs, still verify how Chromium resolves them after you load the markup.
Keep rendered content safe
Prefer ordinary templates, escaped interpolations, and Rails helpers for structured content. Rails notes that the html: render option escapes HTML entities unless the value is composed with HTML-safe-aware APIs. Do not mark arbitrary user-provided text as safe just to make it appear in the PDF; render it through normal escaping and use trusted markup only where it is actually intended.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Send markup to a Puppeteer process
A typical separation is a Rails service calling an internal Node HTTP endpoint that accepts HTML and responds with PDF bytes. Keep that endpoint private to the application network and authenticate the request in a production deployment. This small Node example shows the browser stage; the transport and authentication between Rails and Node are application-specific.
Rank #2
- CARTRIDGE-FREE PRINTING — Print lab-quality photos, graphics and creative projects; Get vibrant colors and sharp text with Epson's high-accuracy printhead and Claria ET Premium 6-color inks
- INK BOTTLES — Save on photos1 and creative projects with affordable in-house printing; All-in-one printer allows you to print 4" x 6" photos for about 4 cents each vs. 40 cents with traditional ink cartridges1
- LESS FREQUENT INK REPLACEMENT — Replacement ink bottles don't have to be changed nearly as often as ink cartridges¹; Printer, scanner and copier lets you print up to 6,200 color pages³
- PRINT FOR LONGER — Up to 2 years of ink in the box² (and with every replacement ink set) for fewer out-of-ink frustrations with this wireless printer
- ZERO CARTRIDGE WASTE — Epson EcoTank printer helps reduce the amount of cartridge waste ending up in landfills; Cartridge-free printer uses high-yield ink bottles; Each replacement ink bottle set is equivalent to about 100 individual ink cartridges⁴
// pdf-renderer.mjs
import puppeteer from "puppeteer";
export async function renderPdf(html) {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle2" });
const pdf = await page.pdf({
format: "A4",
printBackground: true
});
return Buffer.from(pdf);
} finally {
await browser.close();
}
}
page.setContent() loads markup rather than navigating to an application URL. The networkidle2 setting is a possible readiness strategy, not a universal guarantee: scripts or third-party assets may continue loading, while some pages may never become idle. Puppeteer’s PDF guide uses networkidle2 in its URL example, but your rendered template needs its own readiness check.
For production, decide whether to keep a browser process alive or launch one per render based on your job volume and process model. The example closes the browser in a finally block so an exception does not skip cleanup. Add request-size limits, timeouts, concurrency controls, and error reporting at the Rails-to-Node boundary; do not let arbitrary users submit unrestricted HTML to a browser process with broad network access.
Choose the page-loading method
| Input | Use when | Trade-off |
|---|---|---|
page.setContent(html) |
Rails already rendered a specific template and you want to print that markup. | Relative assets need a usable base URL or absolute URLs; the full web request and route behavior are not replayed. |
page.goto(url) |
You want Chromium to visit a Rails route and execute the page as a browser would. | The browser needs network access and whatever authentication the route requires. You must ensure the route serves the intended PDF document view. |
For a URL-based approach, replace the content-loading line with await page.goto(url, { waitUntil: "networkidle2" });. Do not expose a renderer endpoint that accepts arbitrary client URLs without considering server-side request risks: Chromium could be directed at resources your application can reach. If access to a document is authenticated, provide an intentional authentication mechanism rather than assuming Puppeteer shares the end user’s Rails session.
Control PDF styling and readiness
Print CSS is the default
page.pdf() uses the print CSS media type by default. Define print-specific page breaks, margins, visibility, and layout in your stylesheet, for example with @media print and @page rules. If you specifically need the screen stylesheet instead, call await page.emulateMediaType('screen') before page.pdf(). Be aware that print rendering can modify exact colors; Puppeteer documents -webkit-print-color-adjust as a way to request exact color handling.
@media print {
.no-print { display: none !important; }
.page-break { break-before: page; }
}
@page {
margin: 18mm;
}
The PDF call accepts options such as paper format, margins, landscape orientation, page ranges, and background printing. The code above sets A4 and enables background printing; choose other settings to match the document rather than relying on browser defaults.
Rank #3
- SET IT UP ONCE AND PRINT WITH CONFIDENCE. No complicated maintenance. Just easy, reliable printing you can count on.
- INK FOR YEARS. NOT MONTHS. Up to 2 years of ink included. Get thousands of pages of cartridge-free printing. More pages, less hassle
- KEEPS PRINTING WELL AFTER COMPETITORS HAVE QUIT. No complex maintenance. Sharper text, richer colors.[2] Only with HP Smart Tank
- PREMIUM SUPPORT - Strong technical expertise to solve issues faster
- THE LAST PRINTER YOU'LL EVER NEED. Enjoy years of refillable, cartridge-free printing.
Wait for what the document actually needs
Puppeteer’s PDF guide states: “By default, the Page.pdf() waits for fonts to be loaded.” That is useful but does not establish that every image, script, stylesheet, or application-specific asynchronous task is ready. The guide’s font behavior and a network-idle condition are not substitutes for validating the finished document with the actual template and assets.
- Use a controlled asset host and absolute asset URLs where needed.
- Wait for a meaningful selector or application signal if content is populated asynchronously.
- For critical images, verify they completed loading before printing.
- Test templates with long content, missing optional data, and page-break boundaries.
A blank or partially styled PDF often means the print happened before an asset was available or the markup referenced a URL that does not resolve from Chromium. Inspect browser console errors and failed network requests in the renderer service rather than adding an arbitrary long delay as the first fix.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsReturn the PDF from Rails
Once Puppeteer returns bytes, send_data sends generated data as a response. Set a meaningful filename and the MIME type to application/pdf. The example uses attachment disposition to prompt a download; use inline disposition if browser display is the desired behavior.
send_data pdf_bytes,
filename: "invoice-#{invoice.id}.pdf",
type: "application/pdf",
disposition: "attachment"
Use send_file when the PDF is already stored on disk. Rails cautions that file paths must be handled safely; do not build a filesystem path from unchecked client input. For generated in-memory content, send_data is the direct fit.
Performance, reliability, and cost considerations
The cited Rails and Puppeteer documentation does not establish a universal rendering time, capacity, or reliability figure for this integration. Actual resource use depends on the document, assets, Chromium runtime, and how the browser process is managed. Treat PDF generation as work that can fail independently of the initial Rails render, and make those failure modes visible.
Rank #4
- Wireless Bluetooth Printer: Portable thermal printer compatible with iPhone, Android phones, iPad and tablet computers via Bluetooth. For smartphones, please download the "Nada Print" App. You can also connect to laptops and computers for printing using a USB-C cable. (Note: Laptops and computers can only be connected via USB and require the installation of a driver first. Bluetooth connection is not supported.)
- No-ink printing: Only supports US Letter and A4 size thermal paper.(Doesn't support regular paper) The no-ink portable thermal printer uses direct thermal technology, requiring no ink, toner or ribbons, making it environmentally friendly, cost-effective and time-saving. The thermal printer package comes with a roll of US Letter thermal printing paper. Note: When installing the paper, remember to switch the paper size switch on APP
- Clear Print: NDYIN N80 portable thermal printer adopts high-definition printing technology, with a 203DPI resolution to provide you with clear printing results. This mobile printer is compatible with roll paper, folded paper and tattoo transfer paper, supporting printing from your mobile phone PDF, Word, pictures and web pages anytime and anywhere. It is recommended to use our NDYIN thermal paper to achieve good printing quality
- Portable wireless printer for travel: The thermal printer is equipped with a built-in 1500mAh rechargeable battery, which can print 160 sheets of 8.5" x 11" thermal paper after being fully charged. It weighs only 1.5 pounds and is compact in size. This ink-free portable printer can be easily carried in a backpack or briefcase! It is perfect for business travel, cars, small offices, construction sites, schools and homes. You can print documents, contracts, invoices and boarding passes anytime and anywhere
- The N80 thermal printer has a wide range of uses. The package includes the N80 printer, a roll of US Letter paper(7m/roll), a user manual, a guide card, a type-C soft cable and a type C adapter. Note: The charging adapter is not included. Special thermal paper is required for use; ordinary paper cannot be used. This ink-free portable thermal printer is suitable for various scenarios such as home, school, travel, office, and outdoor, meeting the printing needs of different groups of people. This tattoo template printer is also compatible with tattoo transfer paper, making it an ideal choice for tattoo art
- Set timeouts for the Rails-to-renderer request and for browser navigation or readiness.
- Limit simultaneous renders so a burst of PDF requests does not overwhelm the Node process.
- Log a request or document identifier through Rails and the renderer, but avoid logging sensitive document bodies.
- Decide how Rails responds to renderer failures: return an error, retry an idempotent job, or generate asynchronously for large documents.
- Measure your own representative documents under the deployment conditions you intend to use before setting capacity expectations.
Troubleshooting common failures
| Symptom | Likely cause | What to check |
|---|---|---|
| Missing CSS, images, or fonts | Relative URLs resolve differently from the browser context, or Chromium cannot reach the asset host. | Render absolute URLs with the correct host and HTTPS context; inspect Chromium network failures. |
| PDF has default or screen-looking layout | The template does not define print rules, or screen media was explicitly selected. | Check @media print, @page, and whether emulateMediaType('screen') is called. |
| Some dynamic content is absent | Printing started before app-specific JavaScript finished. | Wait for a known selector or explicit readiness signal and confirm the script did not fail. |
| Rails receives an error instead of a PDF | The Node endpoint failed, timed out, or returned an error payload. | Log renderer status and distinguish an error response from PDF bytes before calling send_data. |
| Broken links or wrong absolute asset host | Standalone rendering used a mocked request with unsuitable host or protocol values. | Set renderer http_host and https to the appropriate deployment context. |
| Unsafe or unexpectedly interpreted text | Untrusted content was marked HTML-safe or inserted as raw markup. | Use normal Rails escaping and trusted templates; do not apply html_safe to arbitrary input. |
Or skip the browser setup
If your Rails page is already reachable at a URL and your goal is a clean capture rather than a custom server-side Puppeteer pipeline, ScreenshotNeo offers a one-request screenshot API. This does not send a Rails HTML string to Puppeteer: the API captures a URL. Its provided request example returns an image file, so use it for a screenshot workflow, not as a drop-in replacement for this article’s custom PDF byte-generation flow.
Free tools Windows power users keep installed
One-click scans. No signup required.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for its request options. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.
Sign up for the free plan to try it without a card.
Frequently Asked Questions
Can Rails render the HTML without returning it to the browser first?
Yes. A controller can call render_to_string, and code outside a controller action can use ApplicationController.renderer.render; both produce a string for the browser stage.
Does Puppeteer print PDF using screen styles by default?
No. page.pdf() defaults to print media. Use page.emulateMediaType('screen') before printing only when screen media is specifically desired.
Recommended Free Tools
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




