How to Apply JavaScript from a String When Generating a PDF in Ruby
Run JavaScript from a Ruby string before PDF rendering with Wicked PDF or PDFKit, wait for completion, load assets reliably, and troubleshoot failures.
To apply JavaScript stored in a Ruby string, put that string inside a complete HTML document and render the HTML with a browser-based PDF engine. With Wicked PDF, the usual sequence is: build the HTML, enable JavaScript, wait for your script to finish, then write the returned PDF bytes to a file.
Wicked PDF and PDFKit invoke wkhtmltopdf, so they can execute page JavaScript. Prawn follows a different model: it draws PDF primitives directly and does not execute DOM JavaScript. Choose Prawn only when the document can be calculated and laid out entirely in Ruby.
1. Complete Wicked PDF example
This example updates a DOM element from an inline JavaScript string, signals completion through window.status, and asks wkhtmltopdf to wait for that signal.
js = <<~JS
(function () {
const node = document.getElementById('total');
node.textContent = '42';
window.status = 'js-finished';
}());
JS
html = <<~HTML
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Report</title>
</head>
<body>
<h1>Invoice</h1>
<div id="total">Calculating…</div>
<script>#{js}</script>
</body>
</html>
HTML
pdf = WickedPdf.new.pdf_from_string(
html,
enable_javascript: true,
javascript_delay: 500,
window_status: 'js-finished'
)
File.binwrite('report.pdf', pdf)
pdf_from_string accepts the HTML string. The wrapper option names can differ by Wicked PDF version, so inspect the generated command and the options supported by the installed version before deploying.
2. Install and configure the renderer
- Install the Wicked PDF gem in your Rails application.
- Install a compatible
wkhtmltopdfbinary on every machine that generates PDFs. - Verify the binary available to the application process, not only the one available in your interactive shell.
- Render a small fixture containing the same JavaScript, CSS, fonts, and images used by your production document.
Wicked PDF runs wkhtmltopdf outside the Rails process. Relative asset paths commonly fail when the renderer runs from another working directory or in a production asset setup. Use absolute URLs or Wicked PDF’s JavaScript, stylesheet, and image helpers. Ensure the renderer can reach those URLs from the deployment network.
3. Choose a JavaScript synchronization method
| Method | Use it when | Trade-off |
|---|---|---|
javascript_delay |
The page needs a known, measured amount of time after loading. | Simple, but a fixed delay can be too short or unnecessarily slow. |
window_status |
You control the page and can signal after data and DOM updates complete. | More deterministic; the page must set the exact status value. |
run_script |
You need a small post-load action injected by the renderer. | Availability and naming depend on the wrapper version. |
Fixed delay
wkhtmltopdf documents a 200 ms default JavaScript delay. Increase it only when the measured page work needs more time.
pdf = WickedPdf.new.pdf_from_string(
html,
enable_javascript: true,
javascript_delay: 1000
)
Completion signal with window.status
For pages you control, set the status after the final asynchronous operation and DOM mutation. The value must exactly match the renderer option.
<script>
fetch('/api/total')
.then(response => response.json())
.then(data => {
document.querySelector('#total').textContent = data.total;
window.status = 'js-finished';
});
</script>
pdf = WickedPdf.new.pdf_from_string(
html,
enable_javascript: true,
window_status: 'js-finished'
)
If the status is never set, the renderer can wait until its own timeout or finish without the updated content, depending on the wrapper and binary version. Add an explicit fallback delay or timeout policy at the application layer.
4. Inject a string safely
Interpolating JavaScript into a heredoc is convenient, but values inserted into the script must be encoded for JavaScript. Do not place untrusted text directly inside a quoted JavaScript string.
require 'json'
name = 'Ada'
js = <<~JS
const name = #{JSON.generate(name)};
document.querySelector('#name').textContent = name;
window.status = 'js-finished';
JS
Keep the script in an inline <script> element inside a complete document. Include a charset declaration and valid HTML structure so the renderer parses the page consistently.
5. PDFKit variant
PDFKit is another Ruby wrapper around wkhtmltopdf. The same HTML and JavaScript model applies; configure JavaScript and synchronization through the option names exposed by your installed PDFKit version.
require 'pdfkit'
kit = PDFKit.new(
html,
enable_javascript: true,
javascript_delay: 500,
window_status: 'js-finished'
)
File.binwrite('report.pdf', kit.to_pdf)
Check the generated wkhtmltopdf command when an option appears to have no effect. Wrapper versions may translate Ruby keys differently, and the installed binary may not support every flag.
6. When Prawn is the better choice
Prawn creates a PDF directly from Ruby. It is appropriate when all values are already available in Ruby and you do not need browser CSS, DOM manipulation, or JavaScript.
require 'prawn'
Prawn::Document.generate('report.pdf') do |pdf|
pdf.text 'Invoice'
pdf.move_down 12
pdf.text 'Total: 42'
end
There is no HTML page for JavaScript to mutate in this model. Move the calculation into Ruby or use an HTML renderer when the result depends on browser code.
7. Loading CSS, images, fonts, and data
- Use absolute asset URLs or the helpers provided by Wicked PDF.
- Make private assets reachable using the renderer’s authentication mechanism, such as appropriate headers or cookies.
- Confirm that DNS, TLS certificates, firewalls, and proxy settings work from the worker host.
- Wait for data requests before assigning
window.status. - Keep JavaScript, CSS, and images available for the entire render; short-lived signed URLs can expire before capture.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| JavaScript changes are missing | JavaScript is disabled, the delay is too short, or the completion status is never set. | Enable JavaScript, increase the measured delay, or set the exact window.status value after the update. |
| PDF contains “Calculating…” | The asynchronous request has not completed before printing. | Set the completion signal inside the request’s success path and wait for it. |
| Styles or images are absent | Relative paths resolve differently in the external renderer. | Use absolute URLs or Wicked PDF asset helpers and verify network access from the worker. |
| Works locally, fails in production | Different wkhtmltopdf builds, missing fonts, permissions, or environment variables. | Record the binary version, compare generated commands, install required assets, and render in the same deployment image. |
| Page hangs | A status value is never reached or a request never resolves. | Add request timeouts, guarantee a success and failure completion path, and use a bounded delay or job timeout. |
| Modern browser code behaves differently | wkhtmltopdf’s rendering engine is not identical to a current browser. | Simplify unsupported features, test against the deployed binary, or use a renderer with the browser capabilities your page requires. |
9. Reliability, performance, and cost
- Reliability: Pin and record the wkhtmltopdf build, render from a repeatable environment, and log the generated command and renderer errors.
- Synchronization: Prefer a completion signal for pages you own. Use a fixed delay only after measuring the required work.
- Performance: Avoid unnecessary third-party assets and long delays. Reuse prepared HTML and keep the document’s data requests bounded.
- Security: Treat interpolated values as data, encode them for JavaScript, and avoid exposing secrets in HTML or URLs.
- Cost: Self-hosted rendering costs compute, storage, and operational time. A hosted capture service can shift browser maintenance to the provider; check its billing rules and failure behavior.
10. Or skip the browser setup
ScreenshotNeo provides a single GET request for a clean screenshot or PDF, with options for full-page capture, waiting, custom JavaScript, headers, cookies, and other capture controls. See the ScreenshotNeo API documentation for PDF parameters and the complete option list.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. ScreenshotNeo also provides an MCP server for AI agents, including Claude and Cursor, with screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with the monthly free quota.
FAQ
Can I execute JavaScript with Prawn?
No. Prawn writes PDF primitives directly. Use Wicked PDF or PDFKit when a DOM and browser-style JavaScript execution are required.
Is a delay always required?
No. A completion signal such as window.status is preferable for pages you control. A measured delay is useful when you cannot add a signal.
Why does browser output differ from the PDF?
The PDF is rendered by the installed wkhtmltopdf build, which may support different browser features and fonts. Validate against that exact binary in the deployment environment.
Where should the JavaScript string live?
Put it in an inline <script> element inside a complete HTML document passed to the HTML-to-PDF renderer.


