Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use an absolute stylesheet URL in the HTML you give to the renderer:

<link rel="stylesheet" href="https://cdn.example.com/app.css">

The renderer must be able to resolve and fetch that URL from its own runtime environment. In Rails, generate the link with stylesheet_link_tag; in out-of-process PDF tools, configure a real base URL, precompile assets where necessary, and verify network access from the rendering host.

The reliable pattern: an absolute URL

An absolute https:// URL is the safest common denominator when Ruby renders HTML through a separate process such as wkhtmltopdf or Chromium. Relative references such as /assets/report.css only work when the renderer has a correctly configured document origin.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://cdn.example.com/app.css">
  </head>
  <body>
    <h1>Invoice</h1>
  </body>
</html>

Use a URL that is reachable from the machine, container, or worker actually performing the render—not merely from your laptop or Rails web process. Confirm DNS, TLS, firewall rules, authentication, redirects, and the response content type.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall

Rails HTML rendering with stylesheet_link_tag

Rails’ stylesheet_link_tag returns <link> tags for the supplied sources. It accepts an asset name, a path relative to the document root, or a URL.

Reference a remote stylesheet

<%= stylesheet_link_tag "https://cdn.example.com/app.css" %>

For a PDF-specific layout, keep the stylesheet reference in that layout so the same HTML path is used consistently by the PDF renderer.

Use the Rails asset pipeline

Stylesheets can live under app/assets, lib/assets, or vendor/assets. A typical view uses:

<%= stylesheet_link_tag "application", media: "all" %>

That is convenient for browser responses, but an external renderer still needs the generated asset URL to resolve and the asset to exist in the deployment. Production PDF jobs commonly require precompiling the CSS used by PDF views. If the asset is private, requires a session, or is served only on an internal hostname, the renderer may receive a login page or an error instead of CSS.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wicked PDF and wkhtmltopdf

Wicked PDF runs wkhtmltopdf outside the Rails application. Its documentation requires absolute references for CSS, JavaScript, and images when those resources are used.

Use the Wicked PDF helper

<!-- app/views/layouts/pdf.html.erb -->
<!doctype html>
<html>
  <head>
    <%= wicked_pdf_stylesheet_link_tag "pdf" %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Ensure the PDF stylesheet is precompiled in production. For a small, stable stylesheet, inlining it or using a base64 representation can avoid a second network request, but large CSS is usually easier to maintain as a compiled asset.

Emit a fully qualified URL

<link rel="stylesheet" href="https://files.example.com/assets/pdf.css">

Do not assume that a URL such as /assets/pdf.css is meaningful to a process with no browser origin. Make the host and scheme explicit.

Protect untrusted content

Wkhtmltopdf can request network resources while rendering. Sanitize user-generated HTML and CSS, or restrict requests so untrusted input cannot reach internal IP addresses and hostnames. This is both a correctness and server-security requirement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

PDFKit: supply stylesheets and a base URL

PDFKit wraps wkhtmltopdf and exposes stylesheet and URL options.

Add a stylesheet by filesystem path

kit = PDFKit.new(html)
kit.stylesheets << "/path/to/css/file.css"
pdf_bytes = kit.to_pdf

This approach bypasses remote fetching for the CSS file. The path must exist on the host running the job.

Resolve relative URLs with root_url and protocol

kit = PDFKit.new(
  html,
  root_url: "example.com",
  protocol: "https"
)
pdf_bytes = kit.to_pdf

With these options, relative image, font, and stylesheet paths can resolve against a known origin. PDFKit notes that stylesheet options cannot be added when the source is supplied as a URL or a File; put the <link> inside that source document or pass an HTML string instead.

Grover and Chromium

Grover uses Chromium and supports stylesheet injection as a URL, local path, or inline content.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Inject a URL, path, or CSS string

grover = Grover.new(
  html,
  style_tag_options: [
    { url: "https://cdn.example.com/app.css" },
    { path: "app/assets/builds/pdf.css" },
    { content: "body { color: #222; }" }
  ]
)
pdf = grover.to_pdf

Use only the option you need; the example shows the three supported forms. When calling Grover directly, set display_url or preprocess relative paths. Chromium needs a base URL, and without one Grover defaults to http://example.com, which may not match your deployment.

grover = Grover.new(
  html,
  display_url: "https://app.example.com/reports/42"
)
image = grover.to_png

A deliberately chosen display URL also gives relative fonts, images, and CSS a predictable origin.

Choose the implementation by renderer

Renderer Engine CSS injection Base-URL setting Deployment concern
Rails view Application HTML response stylesheet_link_tag, URL, asset name, or path Browser/request URL Make compiled assets available in the environment serving the view
Wicked PDF / wkhtmltopdf WebKit-based executable wicked_pdf_stylesheet_link_tag or absolute <link> Prefer fully qualified URLs Precompile PDF CSS; restrict untrusted network requests
PDFKit wkhtmltopdf kit.stylesheets or a link in HTML root_url and protocol Stylesheet options do not apply to URL/File sources
Grover Chromium style_tag_options: URL, path, or content display_url Without a base, relative URLs use Grover’s default origin

There is no universal speed or fidelity ranking among these renderers. Output depends on the engine, CSS, fonts, JavaScript, network conditions, and options used.

A repeatable debugging checklist

  1. Inspect the final HTML. Confirm that the emitted <link> contains the expected absolute https:// URL, or that a local path is present on the renderer host.
  2. Fetch from the renderer environment. Run an HTTP request from the same container, VM, or worker. Check DNS resolution, TLS certificates, proxy settings, firewall policy, and authentication.
  3. Check the response. Verify a successful status and CSS content type. A redirect to a sign-in page, an HTML error document, or a blocked request often appears as “CSS ignored.”
  4. Set the correct base. Use PDFKit’s root_url/protocol or Grover’s display_url whenever the document contains relative URLs.
  5. Verify asset deployment. Confirm that the PDF bundle was precompiled and that the generated filename matches the link emitted by Rails.
  6. Check renderer logs. Look for certificate failures, DNS errors, blocked requests, missing files, and timeouts. Capture a minimal HTML document with one stylesheet to isolate the failing layer.
  7. Remove conflicting CSS assumptions. A stylesheet can load successfully yet appear ineffective because a later rule, selector specificity, media query, or unsupported browser feature overrides it.
  8. Restrict untrusted input. Sanitize HTML/CSS and deny access to internal destinations when users can influence rendered content.

Common failures and precise fixes

“The link works in Chrome but not in the PDF”

The PDF process may not share your browser’s cookies, DNS, proxy, or network route. Publish the CSS at a renderer-reachable HTTPS URL, or provide a local stylesheet path through PDFKit/Grover.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“Relative images and fonts are missing too”

That indicates a missing or incorrect document origin. Set root_url and protocol in PDFKit, or display_url in Grover; alternatively rewrite resources to absolute URLs.

“Rails generated a fingerprinted file that returns 404”

Precompile the relevant assets in the production build and deploy the generated files wherever the renderer fetches them. Check the exact URL in the rendered HTML rather than guessing the filename.

“The stylesheet URL returns 200, but styles do not apply”

Inspect the response body and content type. A 200 response containing an authentication page or proxy notice is not CSS. Also check media attributes and CSS ordering.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is a clean screenshot or PDF of the rendered page rather than managing a Ruby renderer, ScreenshotNeo provides a single HTTP endpoint. It accepts the consent banner like a visitor, removes more than 60 known consent platforms plus newsletter popups and chat widgets before capture, and bills only clean shots. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the page verdict and billing status in headers.

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 documentation for all options. You can also use the supplied Ruby application as the caller:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
require "net/http"
require "uri"

uri = URI("https://api.screenshotneo.com/v1/shot")
uri.query = URI.encode_www_form(access_key: ENV.fetch("SCREENSHOTNEO_KEY"), url: "https://stripe.com")
File.binwrite("shot.webp", Net::HTTP.get(uri))

Other client examples

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}`);

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Every plan includes the full feature set, including selector capture, full-page lazy-image loading, custom CSS and JavaScript, waits, request blocking, cookies, headers, device and viewport controls, PDF settings, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, and a usage API. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up free.

FAQ

Can a stylesheet URL require authentication?

Yes, but the renderer must receive the required credentials through its supported headers, cookies, or a controlled local asset. A browser session on your workstation does not automatically carry over to wkhtmltopdf or Chromium.

Should I inline all CSS for PDF generation?

Inlining can remove a network dependency for small stylesheets, but it increases document size and complicates maintenance. Use it selectively; otherwise serve a precompiled stylesheet or inject a local file.

Why does a modern CSS rule work in a browser but not in my PDF?

Different engines support different CSS and JavaScript features. Identify whether the job uses wkhtmltopdf/WebKit or Chromium, then adapt the stylesheet or choose a renderer whose engine supports the required feature.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can a stylesheet URL require authentication?

Yes, but the renderer must receive the required credentials through its supported headers, cookies, or a controlled local asset. A browser session on your workstation does not automatically carry over to wkhtmltopdf or Chromium.

Should I inline all CSS for PDF generation?

Inlining can remove a network dependency for small stylesheets, but it increases document size and complicates maintenance. Use it selectively; otherwise serve a precompiled stylesheet or inject a local file.

Why does a modern CSS rule work in a browser but not in my PDF?

Different engines support different CSS and JavaScript features. Identify whether the job uses wkhtmltopdf/WebKit or Chromium, then adapt the stylesheet or choose a renderer whose engine supports the required feature.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.