ScreenshotNeo

BlogHow-to

Fix Website Screenshots with Missing Fonts in Puppeteer

Diagnose missing fonts in Puppeteer screenshots by checking capture timing, web-font requests, CSS, runtime fonts, and glyph coverage.

By the ScreenshotNeo team4 October 20267 min read

If a Puppeteer screenshot shows the wrong font, first wait for your page’s real content-ready signal and then await document.fonts.ready before capturing. If the result is still wrong, inspect font and stylesheet requests, the selected CSS family and weight, the deployed browser’s installed fonts, and whether the intended face covers every character. An extra delay or network-idle wait cannot fix a font that failed to load, is missing from the runtime, or lacks the needed glyphs.

A screenshot records the browser’s rendered state at capture time. Font readiness is a timing signal: it means current font loading and associated layout have settled, but it does not prove a particular font loaded successfully or supplied every glyph. [Puppeteer screenshot guide, MDN FontFaceSet.ready]

1. Wait for the page and its fonts before capture

Use a readiness condition that corresponds to the content you intend to screenshot, then await the browser’s font set. Replace the example selector with a marker your application actually sets; remove that wait if the page has no such marker.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'load' });
    await page.waitForSelector('[data-render-complete]');
    await page.evaluate(() => document.fonts.ready);
    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

page.evaluate waits for a promise returned by the page function, so returning document.fonts.ready makes the browser-side promise awaitable in Puppeteer. [Puppeteer Page.evaluate, MDN FontFaceSet.ready]

Choose navigation readiness deliberately

Condition Use it for What it does not guarantee
domcontentloaded Pages where scripts or app logic continue after parsing. That the page, images, stylesheets, or fonts have finished loading.
load A conventional page-load starting point. That later app rendering or font loading is complete.
networkidle0 / networkidle2 Pages where network quiet is a useful additional signal. That the right font loaded or that the intended face has glyph coverage. Persistent connections and later app work can complicate the signal.
App-specific marker Content rendered when your application says it is ready. That fonts and glyphs are correct; follow it with the font wait and diagnostics.

Puppeteer’s screenshot guide shows navigation with a network-idle condition followed by a screenshot, but it is an example workflow, not proof that a specific CSS face rendered. [Puppeteer screenshot guide]

2. Check whether the web font actually loaded

Open browser request and response logs for the stylesheet and font files. Verify the requested URL, response status, cross-origin access, server configuration and MIME type. Then check that the CSS rule matches the text’s requested family, weight, style and subset. A font-family declaration is a preference list; by itself, it does not establish which face rendered.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    page.on('requestfailed', request => {
      console.error('request failed:', request.url(), request.failure()?.errorText);
    });
    page.on('response', response => {
      const type = response.request().resourceType();
      if (type === 'font' || type === 'stylesheet') {
        console.log(type, response.status(), response.url());
      }
    });
    await page.goto('https://example.com', { waitUntil: 'load' });
    await page.waitForSelector('[data-render-complete]');
    await page.evaluate(() => document.fonts.ready);

    const state = await page.evaluate(() => {
      const heading = document.querySelector('h1');
      return {
        fontStatus: document.fonts.status,
        headingFont: heading ? getComputedStyle(heading).font : null,
        headingFamily: heading ? getComputedStyle(heading).fontFamily : null,
        headingWeight: heading ? getComputedStyle(heading).fontWeight : null,
        headingStyle: heading ? getComputedStyle(heading).fontStyle : null,
      };
    });
    console.log(state);
    await page.screenshot({ path: 'page.png' });
  } finally {
    await browser.close();
  }
})();

The computed style is a useful clue about CSS selection, not definitive proof of the actual face chosen for every shaped glyph. Inspect the captured output using representative text, including characters from the affected script.

Inspect the CSS font face and fallback chain

  • Confirm the element’s computed font-family, font-weight, and font-style match an available @font-face declaration.
  • Check that the font URL and any unicode-range subset cover the characters in the screenshot.
  • Check CSS overrides, delayed stylesheet injection, and whether the expected stylesheet itself failed.
  • Keep a fallback stack that contains a suitable face for the scripts your content uses.

font-display controls how fallback and downloaded faces are shown during loading and after a load failure. Depending on its value and timing, a screenshot may capture fallback text while the CSS names a web font. [MDN font-display]

3. Check fonts installed in the deployed browser runtime

Web fonts delivered by a page and fonts installed in the host environment are separate paths. If the design relies on local/system fonts, or fallback characters are absent, inspect the exact Linux container or deployment image that launches Chrome. Puppeteer’s troubleshooting guidance documents Linux runtime dependencies and examples of installing font packages for different scripts. Choose packages based on the scripts needed and the distribution in use. [Puppeteer troubleshooting]

  1. Record the container or machine image used for the capture.
  2. Inspect its installed font packages and available font files.
  3. Identify the scripts and symbols present in the affected text.
  4. Install or bundle appropriate fonts in the runtime when system-font coverage is required, then restart the capture process and compare output.

Installing an arbitrary font package may not add the glyphs you need. Verify the actual scripts and representative output.

4. Diagnose missing individual glyphs

If Latin text looks correct but Arabic or another script is wrong, investigate glyph coverage and fallback specifically. A preferred face can render some characters while the browser selects a fallback for others. Verify that the font file contains the affected characters and that the fallback chain offers a face that does.

A Puppeteer issue filed in 2022 reported Arabic glyph fallback in Google Cloud Functions even after network-idle and document.fonts.ready waits. The report was unconfirmed and closed as not planned; it is a case report illustrating why runtime and coverage belong in the diagnosis, not evidence of a general Puppeteer defect. [Puppeteer issue #8264]

5. Compare local and deployed captures

Render the same URL locally and in production with matching Puppeteer and Chrome versions where possible. Record the deployment image, installed font packages, browser version, locale, viewport and device scale factor, and relevant cache or network conditions. If font requests succeed but only certain characters differ, focus on glyph coverage and fallback. If the deployed image differs, investigate its runtime font setup. This comparison helps isolate whether the cause belongs to the page, browser environment, or capture timing.

Evidence Likely area to investigate
Font request fails or stylesheet is missing URL, response, cross-origin policy, server configuration, CSS delivery.
Font request succeeds, but capture is early Application readiness signal and font-loading wait.
Only deployed captures differ Runtime image, installed fonts, Chrome/Puppeteer versions, locale.
Only particular characters differ Glyph coverage, unicode subsets, fallback stack and shaping.

Common errors and fixes

Symptom or mistake Cause Fix
Adding a fixed delay seems to help inconsistently Timing varies, or the font is failing for a reason a delay cannot address. Wait for app readiness and document.fonts.ready; inspect the actual requests and CSS face.
Waiting for network idle does not change the image Network quiet does not establish font success or character coverage. Check font responses, face declarations, deployed fonts and representative glyphs.
document.fonts.ready resolves, but text remains wrong The loading set settled, possibly with a fallback face or unavailable font. Treat readiness as timing evidence only; inspect CSS, requests and coverage.
Computed family names the expected font, but output differs CSS expresses a preference; a face may fail, mismatch weight/style, or lack a glyph. Inspect font file responses and @font-face; check fallback for affected characters.
Works locally, fails in Docker or a cloud function Different browser/runtime image or missing system-font coverage. Compare versions and installed packages; configure the deployed image for the required scripts.
One script or a few symbols are missing Font subset or fallback chain lacks those glyphs. Verify coverage and unicode ranges; add a suitable font to the page or runtime.
Selector wait times out The example marker is not present, has a different name, or app rendering failed. Use a real application-ready signal, or remove the selector wait and diagnose page rendering.

Performance, reliability and cost considerations

  • Capture latency: A readiness marker and font-settling wait can add time, but a guessed fixed delay may wait too long on one run and too little on another. Use the narrowest meaningful application signal.
  • Reliability: Keep the browser image and font packages consistent across workers. Record browser/runtime versions so a deployment change can be compared with earlier captures.
  • Reproducibility: Use the same URL, locale, viewport, device scale, browser versions and representative text when comparing environments.
  • Cost: For self-hosted Puppeteer, account for browser runtime and maintenance, plus the extra capture time and resources consumed by waits. No universal cost or speed figure applies; it depends on the deployment and workload.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its one-call capture can avoid maintaining your own browser setup:

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

See the ScreenshotNeo API documentation for request options. Cookie banners are accepted and removed before the shot, along with known newsletter popups and chat widgets. Bot checks, blank pages and failed loads are never billed. An MCP server lets AI agents use take_screenshot, get_page_info and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, no card required.

FAQ

Does document.fonts.ready confirm my chosen font rendered?

No. It indicates current font loading and layout work settled. Check requests, CSS selection and glyph coverage to determine whether the desired face supplied the text.

Should I always use networkidle0 before a screenshot?

No. It can be useful for some pages, but it is not proof that a font loaded or rendered correctly. Use the page’s real readiness signal and inspect font state when needed.

Why are only some letters wrong?

The selected face or its subset may not contain those glyphs, causing per-character fallback. Check the affected script against font coverage and the fallback chain.

Why does the same page look different in production?

The browser version, runtime image, installed fonts, locale or capture settings may differ. Compare those conditions alongside the font and stylesheet responses.

References