Short answer: page.setContent() parses the HTML string you give it; it is not a disk-file loader and it does not establish a directory base for sibling CSS, JavaScript, images, or fonts. For an existing static site, serve its directory over HTTP and call page.goto(). Keep setContent() for generated markup, using absolute asset URLs or inline/resource-injection methods when appropriate.
What setContent() actually does
Puppeteer’s Page.setContent() method assigns supplied HTML markup to the page. Its API contract does not describe reading an HTML file from disk or mapping relative URLs to a local folder. If your string contains <link href="styles.css">, <script src="app.js">, or <img src="images/logo.png">, those references need a meaningful base URL and a browser-accessible origin.
The cleanest rule is to decide whether you have a site directory or a generated HTML string:
- Site directory: run a local static server rooted at that directory, then navigate to an HTTP URL with
page.goto(). - Generated string: continue using
setContent(), but use absolute URLs or inject the resource content.
Best approach for an existing static folder: serve it and use goto()
Serving the folder gives the browser a normal HTTP document URL. Relative references then resolve like they do in production: a page at http://127.0.0.1:PORT/index.html resolves styles.css beside that file and assets/app.js below its directory.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Minimal Puppeteer flow
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://127.0.0.1:PORT/index.html', {
waitUntil: 'load'
});
await page.screenshot({path: 'page.png', fullPage: true});
await browser.close();
})();
Replace PORT with the port used by your static server. The URL includes its scheme, which is required for navigation. Start the server before launching the script, or start it from your test runner and wait until the port accepts connections.
Why HTTP is preferable to file://
A file:// URL can appear convenient, but modern browsers commonly treat file-scheme documents as opaque origins. Linked local files can consequently encounter same-origin restrictions, and behavior varies with browser build and asset type. If you must use file navigation, verify the exact Puppeteer/Chromium version and every resource your deployment needs. An HTTP server is easier to reason about and matches how relative URLs work on a real site.
When setContent() is still the right tool
For an HTML string produced at runtime, there is no directory to serve. Keep setContent() and make dependencies explicit.
Rank #2
Use absolute resource URLs
await page.setContent(`
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="https://example.test/styles.css">
</head>
<body>
<img src="https://example.test/image.png" alt="">
</body>
</html>
`, {waitUntil: 'load'});
Absolute URLs remove ambiguity about the base path. They still must be reachable from the browser, and remote servers may require authentication, custom headers, or a permissive policy.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallInject CSS or JavaScript content
Puppeteer’s addStyleTag() and addScriptTag() APIs accept either a URL or content. This is useful when your build already has the text of a stylesheet or script, or when you deliberately want to load a URL after setting the document.
await page.setContent('<!doctype html><html><body><main id="app"></main></body></html>');
await page.addStyleTag({content: cssText});
await page.addScriptTag({content: javascriptText});
Inlining avoids relative-path resolution for those resources. Images and fonts still need usable URLs or embedded data, and script execution may be asynchronous, so choose a readiness condition rather than assuming insertion means completion.
Rank #3
Waiting for the page state you actually need
For setContent(), Puppeteer documents waitUntil: 'load' as the default. The supported waitUntil values for this method do not include networkidle0 or networkidle2. A load event means the browser reached that lifecycle point; it does not prove that a single-page application finished later rendering or data requests.
Wait for a selector
await page.setContent(html, {waitUntil: 'load'});
await page.waitForSelector('#report-ready');
Wait for an application condition
await page.waitForFunction(() => {
return document.querySelector('[data-state="ready"]') !== null;
});
Wait for a particular response
const dataResponse = page.waitForResponse(response =>
response.url().endsWith('/data.json') && response.ok()
);
await page.setContent(html, {waitUntil: 'load'});
await dataResponse;
Use the condition that represents the next operation: a selector before taking a screenshot, an application state before extracting text, or a specific response before processing data. A fixed delay is a fallback, not proof that the page is ready.
Request interception: powerful, but optional
Do not enable interception merely to make local files load. Use page.setRequestInterception(true) when you need to alter, fulfill, or block requests—for example, serving an asset from memory, replacing an API response, or preventing trackers.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().endsWith('/feature-flag.json')) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({enabled: true})
});
} else {
request.continue();
}
});
Once interception is enabled, every request stalls until your handler calls continue(), respond(), or abort() (unless it is completed from the browser cache). A missing branch can make the page look hung. Always handle all request types, including favicon, stylesheet, font, and preflight requests. Register the handler before navigation so the first document request is covered.
Diagnosing missing CSS, images, scripts, or fonts
1. Log the URLs the browser requests
page.on('requestfailed', request => {
console.error('FAILED', request.url(), request.failure());
});
page.on('response', response => {
if (!response.ok()) console.error(response.status(), response.url());
});
page.on('console', message => console.log('BROWSER', message.text()));
Compare the requested URL with the path your server exposes. A relative reference may resolve against an unexpected base, especially when markup was injected without a document URL.
2. Check the base and path
- For
goto(), confirm the server root and the requested pathname, including capitalization. - For
setContent(), replace relative links with absolute URLs or add a deliberate base URL and verify that the target server serves them. - Check that the server returns the correct content type and does not require credentials the browser does not have.
3. Separate browser errors from application timing
A 404, blocked request, or certificate error is different from a script that has not run yet. Inspect request failures and console output first; then wait for the selector or state your application sets after successful initialization.
Recommended Free Tools
Best Value
Common failure modes and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Styles and images are 404 | The HTML was set without a usable relative base, or the server root is wrong. | Use goto() to an HTTP page in the static directory, or convert URLs to absolute paths. |
| Navigation hangs after enabling interception | A request was never resolved. | Call continue(), respond(), or abort() on every intercepted request. |
| Screenshot shows an empty app | load fired before asynchronous rendering finished. |
Wait for the app’s ready selector, state, or required response. |
file:// assets behave inconsistently |
Opaque file origins and browser security rules. | Serve the directory over loopback HTTP and navigate with goto(). |
| Remote assets fail while local ones work | Authentication, certificate, CORS, DNS, or network policy. | Open the exact URL in the same browser context, inspect failures, and supply required headers/cookies or a test-safe local copy. |
Performance, reliability, and repeatable builds
- Reuse one browser process and create a fresh page per capture or test when isolation matters.
- Serve files from a stable local directory and use deterministic ports in CI; avoid changing the working directory implicitly.
- Prefer selector or response waits over long sleeps, which slow successful runs and still race under load.
- Keep interception rules narrow. Blocking unnecessary fonts, ads, or analytics can speed captures, but blocking a dependency your app needs creates misleading failures.
- Log the final page URL, failed requests, HTTP status, and the readiness condition used. These details make CI failures reproducible.
- Pin and periodically review your Puppeteer/Chromium build. The official API pages displayed Puppeteer 25.12.0 on September 29, 2026; behavior can change in later releases.
Or skip the browser setup
If your goal is a clean screenshot rather than browser automation, ScreenshotNeo accepts one request and returns PNG, JPEG, WebP, or PDF. It removes cookie/consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots; the response identifies the page verdict and billing status in headers.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo documentation for all options, including full-page and element capture, device and retina settings, custom CSS/JavaScript, waits, request blocking, cookies and headers, PDFs, caching, async webhooks, bulk capture, signed links, and the usage API. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can I make setContent() read an HTML file directly?
Read the file in Node.js, then pass its text to setContent(); that still does not create a filesystem base for relative assets. Serve the directory and use goto() when the file has sibling resources.
Does adding a <base> tag solve every relative-asset problem?
It can define URL resolution for markup supplied to setContent(), but the resulting URLs must still be reachable and permitted. An HTTP static server remains the more predictable choice for a complete site.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Why is networkidle0 unavailable with setContent()?
The documented setContent() wait options support lifecycle values such as load, not the networkidle0/networkidle2 values. Wait for an application-specific selector, response, or state instead.
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.

