ScreenshotNeo

BlogHTML to image & PDF

Integrating Paged.js with Ruby on Rails

Build browser previews and automated PDFs with Paged.js in Rails, including assets, print CSS, CLI rendering, debugging, and production options.

By the ScreenshotNeo team30 September 20269 min read

Integrating Paged.js with Ruby on Rails

Short answer: use the Paged.js browser polyfill when Rails users need an interactive paginated preview or a browser print flow. Use the Paged.js CLI with a headless browser when your application must create PDFs unattended, such as background jobs, exports, invoices, or scheduled reports. Rails is responsible for rendering the HTML and serving its CSS, JavaScript, fonts, and images; Paged.js performs the pagination.

Paged.js transforms HTML and print CSS into paginated layouts. Its browser workflow runs inside a web page and lets a reader inspect the result before using the browser’s Save as PDF command. Its CLI workflow runs the same general transformation in a headless browser and writes a PDF from a command line process. The official documentation describes these workflows separately; the Rails wiring below is a practical integration pattern that you should validate against your Rails and Paged.js versions.

Choose the right architecture

Requirement Browser polyfill CLI and headless browser
Interactive preview Best fit Usually unnecessary
User-triggered print Best fit Possible, but adds a server round trip
Background PDF generation Not a good fit Best fit
Scheduled or bulk exports Not a good fit Best fit
Primary integration work Load script, HTML, and CSS in a browser Install CLI dependencies and expose a renderable document

Use the browser mode if a user needs to change content, see page boundaries, or decide when to print. Use the CLI mode if a controller, job, or service needs a deterministic file. In either case, keep layout rules in print CSS where possible. Add JavaScript hooks only for transformations that CSS and source markup cannot express.

Prepare a Rails document for pagination

1. Create a dedicated printable view

Do not paginate your normal application layout accidentally. Create a document layout with only the resources needed by the printed document.

The Rails view supplies HTML and CSS; Paged.js performs pagination in the browser or headless renderer.
The Rails view supplies HTML and CSS; Paged.js performs pagination in the browser or headless renderer.
# config/routes.rb
Rails.application.routes.draw do
  resources :reports, only: [] do
    member do
      get :print
    end
  end
end
# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
  def print
    @report = Report.find(params[:id])
    render layout: "print"
  end
end
<!-- app/views/layouts/print.html.erb -->
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><%= @report.title %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag "print", media: "all" %>
    <%= javascript_include_tag "paged.polyfill", defer: true %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

The exact asset helper depends on your Rails setup. The Rails guides describe import maps, JavaScript bundlers, the asset pipeline, javascript_include_tag, and build output under app/assets/builds. Follow the mechanism already used by your application instead of introducing a second JavaScript delivery system solely for Paged.js.

2. Add print CSS

/* app/assets/stylesheets/print.css */
@page {
  size: A4;
  margin: 18mm 16mm 20mm;

  @bottom-right {
    content: counter(page) " / " counter(pages);
    font-size: 9pt;
    color: #666;
  }
}

html, body {
  margin: 0;
  padding: 0;
  font-family: system-ui, sans-serif;
  color: #111;
}

h1, h2, h3 {
  break-after: avoid;
}

.chapter {
  break-before: page;
}

.keep-together {
  break-inside: avoid;
}

table, figure, pre {
  break-inside: avoid;
}

img {
  max-width: 100%;
  height: auto;
}

Use break-before, break-after, and break-inside to express pagination intent. Test long tables, code blocks, images, and headings with unusual content lengths. A rule that looks correct for one report can create an almost empty page for another.

Browser preview with the Paged.js polyfill

The getting-started workflow requires HTML, CSS, the Paged.js script, and a web server so the polyfill can access the document’s stylesheets. In Rails development, visit the printable route through the normal server rather than opening an ERB file from disk.

<!-- Use the distribution file that your application has installed and serves. -->
<script src="/assets/paged.polyfill.js" defer></script>

After the page loads, Paged.js paginates the document in the browser. The reader can inspect page boundaries and invoke the browser’s print dialog, then choose Save as PDF. Browser print settings still affect the final file, including paper size, margins, headers, footers, and background graphics. Document those settings for users who need consistent output.

Import-map and bundler considerations

  • Import map: Rails can pin JavaScript modules without a build step. Verify that the Paged.js distribution you selected is importable in the form you pin and serve; the reviewed sources do not certify a particular pin configuration.
  • Bundler: Install and import Paged.js through the existing build tool, then make sure the generated file is written to the asset path your layout serves.
  • Standalone asset: Copy or package the browser distribution into the Rails asset structure and include it only on printable pages.

Keep the script loading strategy consistent with your page. If pagination starts before application JavaScript has finished modifying the document, the result can differ between development and production. Render the final content first, then let Paged.js process it.

Automated PDFs with pagedjs-cli

For unattended generation, the Paged.js CLI documentation describes installing pagedjs-cli and pagedjs, then running a command such as:

npm install --save-dev pagedjs-cli pagedjs
npx pagedjs-cli report.html -o result.pdf

The CLI uses a headless browser and runs the pagination script automatically. Your Rails application must make the document and every dependency available to that browser. A simple approach is to render a public or authenticated URL and pass that URL to the renderer. Another is to render HTML to a temporary file and ensure relative stylesheets, fonts, and images resolve from a location the browser can access.

Calling the CLI from a Rails job

# app/jobs/report_pdf_job.rb
class ReportPdfJob < ApplicationJob
  queue_as :default

  def perform(report_id)
    report = Report.find(report_id)
    input = Rails.root.join("tmp", "report-#{report.id}.html")
    output = Rails.root.join("tmp", "report-#{report.id}.pdf")

    html = ApplicationController.renderer.render(
      template: "reports/print",
      layout: "print",
      assigns: { report: report }
    )
    File.write(input, html)

    command = [
      "npx", "pagedjs-cli", input.to_s,
      "-o", output.to_s
    ]
    success = system(*command)
    raise "Paged.js PDF generation failed" unless success && File.exist?(output)

    report.pdf.attach(
      io: File.open(output),
      filename: "report-#{report.id}.pdf",
      content_type: "application/pdf"
    )
  ensure
    File.delete(input) if input && File.exist?(input)
    File.delete(output) if output && File.exist?(output)
  end
end

Adapt this example to your asset and URL strategy. The CLI guide is dated, so confirm current package installation instructions, browser runtime requirements, and supported flags before putting the command into production. Pin versions in your application and reproduce the same Node and browser environment in development, CI, and workers.

Rails asset and URL checklist

  1. Open the printable route in the same environment used for capture.
  2. Confirm that the print stylesheet returns HTTP 200 and the expected content type.
  3. Use absolute or correctly rooted URLs for fonts, images, and imported CSS when rendering outside the Rails request.
  4. Make authentication available to the renderer, or generate a short-lived document URL.
  5. Check that CSP, cookies, redirects, and host authorization do not block the headless browser.
  6. Wait for data-dependent content before pagination begins.
  7. Verify the final PDF in CI with representative long and short documents.

When CSS is not enough: hooks and handlers

Paged.js documents handlers and processing hooks for stages such as document parsing and layout. Use them when you need to add generated content, inspect pages, or transform nodes based on document-specific logic.

class RunningTitleHandler extends Paged.Handler {
  afterPageLayout(pageElement, page, breakToken) {
    const title = document.querySelector("h1")?.textContent;
    if (!title) return;
    const header = pageElement.querySelector(".running-title");
    if (header) header.textContent = title;
  }
}

Paged.registerHandlers(RunningTitleHandler);

Check the handler API against the installed Paged.js version. The official handler documentation reviewed for this guide is dated, and hook names or lifecycle details can change. Keep handlers small, deterministic, and covered by document fixtures.

Common problems and fixes

Symptom Likely cause Fix
No pagination occurs Script is missing, blocked, or loaded before the document is ready Inspect the browser console and network panel; serve the script from Rails and load it after the document is available.
Styles are ignored Stylesheet URL is wrong or inaccessible to the renderer Open the CSS URL directly, check status and content type, and use a URL valid in the headless environment.
Images are blank Relative paths, authentication, lazy loading, or capture before image load Use resolvable URLs, provide required cookies, and wait until images are loaded before pagination.
Fonts fall back Font files are blocked, missing, or loaded after layout Serve font files from the renderable host and wait for font loading before generating the PDF.
Unexpected blank pages Conflicting page breaks, oversized keep-together blocks, or a large margin Inspect break-* rules and reduce or split blocks that cannot fit on one page.
CLI works locally but fails in a job Different Node, browser, filesystem, environment variables, or network access Pin dependencies, log the exact command and exit status, and run the worker in a matching image.
Private content redirects to login Renderer has no session or authorization Use a controlled signed URL or configure the renderer’s authentication mechanism; never expose unrestricted private reports.
Print preview differs from PDF Browser print settings or browser versions differ Standardize paper and margin settings, pin the rendering environment, and compare generated artifacts.

Performance, reliability, and cost

Pagination cost is driven by document size and browser rendering work. Large DOM trees, high-resolution images, web fonts, charts, and scripts increase render time. Reduce unnecessary markup, resize source images, avoid loading application-wide JavaScript on print views, and cache stable assets. For jobs, place PDF generation on a queue and record failures with the report ID, renderer version, input URL or file, and exit status.

Reliability improves when the printable view is deterministic. Freeze data at the start of a job, use a stable timezone, avoid timestamps that change during rendering, and make external dependencies explicit. If a document depends on remote images or APIs, decide whether to proxy or snapshot those resources so a transient network failure cannot alter a financial or legal report.

There is no source-backed benchmark for Rails and Paged.js in the reviewed documentation. Measure your own representative documents, including the largest expected report, and set a job timeout based on those observations. Retain failed HTML where privacy policy permits so layout bugs can be reproduced without rerunning a changing query.

Or skip the browser setup

If your Rails application only needs a clean screenshot or PDF of a URL, ScreenshotNeo provides a single request API. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server so Claude, Cursor, and other MCP clients can call take_screenshot, get_page_info, and capture_pdf.

ScreenshotNeo removes common consent and overlay elements before capture.
ScreenshotNeo removes common consent and overlay elements before capture.

See the ScreenshotNeo API documentation for all options. This Rails example saves the returned WebP:

require "net/http"
require "uri"

uri = URI("https://api.screenshotneo.com/v1/shot")
uri.query = URI.encode_www_form(
  access_key: ENV.fetch("SCREENSHOTNEO_ACCESS_KEY"),
  url: "https://example.com"
)

response = Net::HTTP.get_response(uri)
raise "Screenshot failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

File.binwrite("tmp/example.webp", response.body)
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}`);

Features include full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching with a chosen TTL, signed links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API, and an OpenAPI specification. ScreenshotNeo has 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Does Paged.js replace Rails views?

No. Rails still renders the HTML and serves resources. Paged.js transforms that document into pages in the browser or headless browser.

Can I use the polyfill and CLI in one application?

Yes. A preview route can use the polyfill while a background job uses the CLI, provided both receive equivalent HTML and CSS.

Should pagination rules live in JavaScript?

Usually no. Start with print CSS. Use handlers only for document-specific transformations that CSS cannot express.

Why does a local file behave differently from a Rails URL?

Resource resolution, cookies, CSP, redirects, and browser security differ. Test through the same served URL and runtime that production uses.

Is there an official Rails-specific Paged.js recipe?

The reviewed official sources document Paged.js workflows and Rails asset delivery separately. Treat the connection as an integration you must validate in your application.