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

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Sale
HTML and CSS: Design and Build Websites
  • 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.

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.

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

Inject 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.

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.

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

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
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

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

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.

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.