ScreenshotNeo

BlogHow-to

Why Are Fonts Missing in My Website Screenshot from Puppeteer?

Wait for the fonts your page uses, then check failed requests, CSS, glyph coverage, and the browser environment if the expected typeface is still missing.

By the ScreenshotNeo team4 October 20267 min read

Most Puppeteer screenshots with missing or fallback fonts are captured before the page’s used web fonts finish loading. After navigation, wait for document.fonts.ready before calling page.screenshot(). If the intended typeface is still absent, check font and stylesheet requests, CSS declarations, glyph coverage, and the browser’s operating environment instead of adding a longer arbitrary delay.

networkidle0 and networkidle2 describe network activity during navigation; they do not confirm that a particular typeface rendered. The Font Loading API’s document.fonts.ready promise resolves when font loading and layout operations for fonts used by the document have completed. Puppeteer’s screenshot guide shows navigation followed by a screenshot, and MDN documents the font readiness property.

Wait for used web fonts before taking the screenshot

Install Puppeteer in a Node.js project with npm install puppeteer. Save this as screenshot.js and run it with node screenshot.js https://example.com:

const puppeteer = require('puppeteer');

async function main() {
  const url = process.argv[2];
  if (!url) throw new Error('Usage: node screenshot.js <url>');

  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle2' });
    await page.evaluate(() => document.fonts.ready);
    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

The important addition is await page.evaluate(() => document.fonts.ready). The example uses networkidle2 as a navigation condition, then waits specifically for used fonts and their related layout work. fullPage controls the screenshot area; omit it for a viewport screenshot.

Choose a navigation wait condition

Option What it waits for When to use it
domcontentloaded The document’s DOM content has been loaded. When the page continues loading assets after its markup is ready. Follow it with the specific readiness checks your screenshot needs.
load The page load event. For pages where the load event is a useful navigation milestone. It is not a font-specific guarantee.
networkidle0 No more than zero network connections for at least 500 ms. When the page can reach an idle network state.
networkidle2 No more than two network connections for at least 500 ms. When a page has some ongoing connections but can settle enough to continue.

These lifecycle conditions are defined by Puppeteer’s lifecycle event API. Sites with analytics, streaming, polling, or other persistent requests may not reach a network-idle condition promptly. In that case, use an appropriate navigation condition and wait for the page-specific signal you need, such as document.fonts.ready or a known element. Do not treat a network-idle event as proof that every declared font loaded: fonts declared in CSS but not used may not be fetched.

Diagnose the font state and failed resources

When the readiness wait does not fix the image, inspect the page’s font faces and listen for font loading errors. This diagnostic snippet can be inserted after navigation and before the screenshot:

await page.evaluate(() => {
  document.fonts.addEventListener('loadingerror', (event) => {
    console.error('Font loading error:', event);
  });
});

await page.evaluate(() => document.fonts.ready);

const fontState = await page.evaluate(() => ({
  status: document.fonts.status,
  faces: Array.from(document.fonts, (face) => ({
    family: face.family,
    weight: face.weight,
    style: face.style,
    status: face.status,
  })),
}));
console.log(fontState);

document.fonts.status reports the font set’s status, and each face exposes a status. The CSS Font Loading API also provides loading, loadingdone, and loadingerror events. See MDN’s CSS Font Loading API reference.

For request-level details, enable Chrome DevTools Protocol logging or inspect the page in DevTools’ Network panel. Check both font file requests and the stylesheets that declare them. Confirm the request URL, response status, and whether the response is blocked or inaccessible in the browser context. Puppeteer’s page API can also observe responses:

page.on('response', (response) => {
  const request = response.request();
  if (request.resourceType() === 'font' || request.url().match(/\.(woff2?|ttf|otf)(\?|$)/i)) {
    console.log(response.status(), request.resourceType(), response.url());
  }
});

Register the response listener before page.goto() so it can observe requests made during navigation. A successful HTTP response alone does not prove that the font declaration is correct or that the font includes the characters shown in the screenshot.

Check CSS declarations, glyph coverage, and fallback behavior

If fonts are ready but the wrong face appears, check the page’s CSS and content:

  • Confirm the element’s computed font-family, font-style, and font-weight. A requested weight or style may not match the available @font-face declarations.
  • Verify the @font-face family name and src URL, including any URL base-path assumptions in deployed CSS.
  • Check whether a unicode-range declaration or the font file’s character coverage excludes the affected glyphs.
  • If only some scripts, symbols, or characters look wrong, inspect glyph coverage and the configured fallback families. A ready font can still lack a particular glyph.
  • Compare the computed styles and actual text in the screenshot environment with those in a regular browser session.

document.fonts.ready concerns loading and layout for fonts used by the document. It does not certify that a particular font contains every requested glyph or that the page selected the intended family.

Check Puppeteer, Chrome, and the Linux environment

Record the installed Puppeteer and browser versions when a screenshot differs across machines. Puppeteer’s supported browser pairing can change over time; its current guidance says Puppeteer v20 and later downloads Chrome for Testing, while old headless mode uses a separate chrome-headless-shell option. Consult the current Puppeteer supported browsers page and note the actual versions and launch mode used by your job.

In Linux containers, missing shared libraries can prevent Chrome from running correctly, and absent system fonts can affect rendering when the page relies on locally installed fonts or when fallback is needed. Puppeteer’s troubleshooting guide recommends using ldd on Chrome to identify missing libraries and notes that rendering for some languages may require additional font files. Check the container’s installed dependencies and fonts as well as the web font requests.

Troubleshooting common symptoms

Symptom Likely cause What to do
Fallback font appears in the whole page The screenshot ran before a used font finished loading, or the font resource or declaration failed. Wait for document.fonts.ready; then inspect the font request and @font-face declaration.
Network-idle wait hangs or times out Persistent requests keep the page from meeting the selected idle condition. Use a suitable navigation milestone, then await the font readiness promise and any page-specific element needed for a stable capture.
Some letters or one language look wrong The selected face may not cover those glyphs, or the fallback chain may be unsuitable. Check font character coverage, any unicode-range, and fallback families.
Font works locally but not in CI or a container Different browser versions, launch modes, dependencies, fonts, or request access may change rendering. Record versions and mode; inspect browser logs, font requests, Linux libraries, and installed fonts.
document.fonts.ready resolves, but the expected family is absent The promise covers fonts used by the document, not unused declarations or guaranteed glyph coverage. Inspect computed styles, the face list and statuses, the request responses, and glyph coverage.

Or skip the browser setup

If you need a screenshot rather than a local Puppeteer workflow, ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns an image or PDF. See the ScreenshotNeo API documentation for the available parameters.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.

Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card.

Performance, reliability, and cost

Waiting for font readiness adds only the time the page needs to finish loading and laying out its used fonts; a fixed sleep can wait too little on a slow run and waste time on a fast one. Network-idle waits may take longer on pages with persistent connections, so choose the navigation condition based on the site and keep the font-specific wait explicit. For reliable comparisons, hold the URL and CSS constant and record font request outcomes, font readiness, Puppeteer and browser versions and mode, operating system, and available system fonts.

For a self-hosted Puppeteer job, there is no screenshot API charge in this workflow, but you operate the browser and its environment and account for the time and compute the capture uses. The source documentation does not provide a performance benchmark or quantify how frequently each failure cause occurs. ScreenshotNeo’s published plans are Free: 1,000 shots per month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; and Business: $249 for 1,000,000. Yearly billing gives two months free. Its billing rule is that only clean shots are billed.

FAQ

Does networkidle2 guarantee that web fonts have loaded?

No. It is a network lifecycle condition, not a check for a particular typeface. Await document.fonts.ready as well.

Does document.fonts.ready load every font declared on the page?

No. It resolves after loading and layout operations for fonts used by the document; unused declared faces may not be fetched.

Should I add a fixed delay before every screenshot?

Prefer an explicit readiness condition. A fixed delay has no connection to whether the page’s fonts loaded on a particular run.

Can font readiness guarantee every character uses the same typeface?

No. Check that the chosen font covers the characters and that the CSS fallback chain is appropriate.

Where can I verify browser compatibility and Linux requirements?

Check the current Puppeteer browser support and troubleshooting pages, because browser pairings and environment requirements are version-sensitive.