How to Load JavaScript from a URL When Generating PDFs in Node.js
Use Puppeteer’s addScriptTag({ url }) to load remote JavaScript, wait for your app’s ready state, then generate a reliable PDF.
Direct answer: Puppeteer loads a remote script with await page.addScriptTag({ url: scriptUrl }). Wait for an application-owned signal that the script has finished rendering, then call page.pdf(). Puppeteer uses print media by default; call page.emulateMediaType('screen') when the PDF should use screen styles.
Complete Node.js example
This example creates a page, injects a trusted script URL, waits for the application to announce that its content is ready, and writes an A4 PDF.
import puppeteer from 'puppeteer';
const scriptUrl = 'https://example.com/app.js';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setContent(`<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body><main id="app"></main></body>
</html>`);
await page.addScriptTag({ url: scriptUrl });
// app.js must set this after its asynchronous DOM work is complete.
await page.waitForFunction(() => window.pdfContentReady === true, {
timeout: 30_000
});
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true
});
} finally {
await browser.close();
}
The global window.pdfContentReady is an application contract, not a Puppeteer-provided value. Have your script set it only after API calls, charts, images, and other required DOM updates are complete.
Install Puppeteer
npm install puppeteer
Pin Puppeteer in your project and consult documentation matching the installed version. The official PDF guide was reviewed for Puppeteer 25.12.0, while the script-injection references were reviewed for 25.10.0.
Choose the page-loading pattern
Inject into HTML you construct
Use page.setContent() when Node.js owns the HTML shell and the remote script fills or transforms it. Inject only after the document exists.
await page.setContent('<main id="app"></main>');
await page.addScriptTag({ url: 'https://example.com/app.js' });
Navigate to a page that already includes the script
If the real page loads the script in its HTML, do not inject a duplicate. Navigate and then wait for the page’s own completion condition.
await page.goto('https://example.com/report', {
waitUntil: 'networkidle2',
timeout: 60_000
});
await page.waitForSelector('#report-ready', { timeout: 30_000 });
await page.pdf({ path: 'report.pdf', format: 'A4' });
networkidle2 means the network became quiet enough for Puppeteer’s navigation heuristic; it does not prove that business logic, polling, or rendering is complete. Prefer an application-owned selector or readiness flag.
What addScriptTag accepts
| Option | Use |
|---|---|
url |
Load a script from a URL. |
content |
Insert JavaScript source directly. |
path |
Load a local file; relative paths resolve from Node’s current working directory. |
type |
Set the script type, such as module. |
The method resolves to a handle for the injected <script> element and runs in the current page or frame context. See the Page.addScriptTag API and FrameAddScriptTagOptions.
Wait for the result your PDF needs
- Selector: wait for a final element such as
#report-ready. - Flag: set
window.pdfContentReady = trueafter all asynchronous work. - Text or count: wait until a known number of rows or a status message appears.
- Fonts:
page.pdf()waits for fonts by default, but that does not cover arbitrary JavaScript or API work.
await page.waitForFunction(
() => document.querySelectorAll('.invoice-row').length === 25,
{ timeout: 30_000 }
);
A fixed delay can help with a page you cannot modify, but it is less reliable than a state-based condition:
await new Promise(resolve => setTimeout(resolve, 2_000));
Control print and screen styling
Puppeteer generates PDFs with print CSS media by default. To use screen rules, emulate screen media before printing.
await page.emulateMediaType('screen');
await page.pdf({
path: 'screen-styled.pdf',
format: 'A4',
printBackground: true
});
PDF generation can adjust colors for printing. Use CSS -webkit-print-color-adjust: exact when exact colors are required, and verify the resulting page breaks and backgrounds. The Puppeteer PDF guide, Page.pdf reference, and PDFOptions reference document the available PDF settings.
Useful PDF options
| Option | Why use it |
|---|---|
format |
Choose a standard paper size such as A4. |
width, height |
Define custom dimensions. |
margin |
Set top, right, bottom, and left margins. |
landscape |
Use horizontal orientation. |
printBackground |
Include CSS backgrounds and colors. |
preferCSSPageSize |
Honor CSS @page size rules. |
pageRanges |
Print selected pages. |
displayHeaderFooter |
Add PDF headers and footers using Puppeteer templates. |
Security boundary for remote scripts
A URL-controlled renderer fetches and executes code selected by that URL. Do not accept arbitrary script URLs from untrusted users. Allow only approved HTTPS hosts, keep rendering credentials out of page scope, isolate the browser from internal services, and restrict outbound network access at the infrastructure layer.
Puppeteer’s security policy states that its powerful capabilities are the caller’s responsibility to use safely and as intended. Request interception can inspect or abort requests, but it is not a complete defense against hostile pages or redirects. Avoid copying --no-sandbox into production without reviewing your Chromium and hosting requirements. See the Puppeteer security policy and the older Chrome request-interception example.
Reliability and performance checklist
- Pin a Puppeteer version and use matching documentation.
- Reuse a browser process when handling multiple jobs, while creating a fresh page per job.
- Set explicit navigation and readiness timeouts.
- Use a deterministic ready signal instead of an unnecessarily long fixed sleep.
- Cache or self-host trusted scripts when appropriate to reduce dependency on a third-party origin.
- Close pages and browsers in
finallyblocks. - Record the target URL, script URL, elapsed time, and failure reason without logging secrets.
- Limit concurrency to the CPU and memory available to Chromium.
Rendering time depends on page JavaScript, network requests, fonts, images, and Chromium resources. There is no universal Puppeteer speed or cost figure in the cited documentation; measure your own workload.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
addScriptTag times out |
DNS, TLS, CSP, blocked host, or unreachable URL. | Check the URL from the renderer, use HTTPS, inspect browser console and request failures, and allow the host. |
| PDF is blank | Printing happened before asynchronous DOM work completed. | Wait for a selector or application-ready flag before calling pdf(). |
| Script runs twice | The page already includes it and code injected a duplicate. | Choose navigation or injection; do not use both for the same script. |
| Screen layout is missing | Print media is the default. | Call page.emulateMediaType('screen'). |
| Colors look muted | Print color adjustment changed output. | Set printBackground: true and review -webkit-print-color-adjust. |
| Charts or images are absent | Resources were still loading or blocked. | Wait for a chart-ready signal or image completion, then print. |
| Navigation never becomes idle | Polling, analytics, or long-lived connections keep requests open. | Use a bounded navigation timeout followed by an application-owned readiness check. |
| Works locally but fails in deployment | Missing Chromium dependencies, sandbox restrictions, or network policy. | Review the deployment image, browser version, sandbox configuration, and egress rules. |
Or skip the browser setup
ScreenshotNeo provides a website screenshot and PDF API when you do not want to maintain Puppeteer and Chromium. It accepts a URL and returns a PDF or image; see the API documentation.
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}`);
Before capture, cookie banners, newsletter popups, and chat widgets are removed. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. 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.
FAQ
Can I load an ES module?
Use await page.addScriptTag({ url: scriptUrl, type: 'module' }) when the page and script are designed for module loading.
Should I use a remote script or inline code?
Use a remote URL for a trusted, versioned application asset. Inline or local code can reduce dependence on an external origin but must still be reviewed and trusted.
Does page.pdf() wait for every request?
No. It handles documented font waiting, but your application must signal when asynchronous rendering is complete.
Can I generate only selected pages?
Yes. Puppeteer’s PDF options include pageRanges; confirm the syntax against the version installed by your project.
Where can I verify the APIs?
Use the official PDF guide, addScriptTag reference, and Page.pdf reference.


