Fix Puppeteer Screenshots with Missing Fonts on SEO Landing Pages
Wait for the page’s fonts before capturing, then check whether the intended font loaded or the runtime is missing required font files.
If a Puppeteer screenshot shows fallback fonts, first wait for the page’s font set to settle, then verify that the intended font is available and actually used. Navigation completion and network idleness are not font-readiness guarantees. If the font still fails, investigate its request and the fonts installed in the browser’s Linux runtime.
This guide uses Node.js. It shows a runnable capture script, diagnostics for late loading versus failed delivery or missing glyph coverage, and fixes for common deployment problems. Puppeteer’s screenshot guide demonstrates navigation followed by Page.screenshot(), but does not promise that screenshots automatically wait for web fonts. Puppeteer’s screenshot guide and network-idle API describe navigation and network timing separately from font readiness.
1. Wait for fonts before taking the screenshot
After navigating, await document.fonts.ready in the page, then check whether the expected font is available for representative text. Capture only after those checks. Replace the URL, font family, sample text, and output path with values for your page.
const puppeteer = require('puppeteer');
async function capture() {
const url = 'https://example.com/landing-page';
const fontFamily = 'Your Web Font';
const sample = 'Representative landing page text';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
page.on('console', message => {
if (message.type() === 'error') console.error('Page console:', message.text());
});
page.on('requestfailed', request => {
console.error('Request failed:', request.url(), request.failure()?.errorText);
});
page.on('response', response => {
if (response.request().resourceType() === 'font' && !response.ok()) {
console.error('Font response:', response.status(), response.url());
}
});
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
await page.evaluate(async () => {
await document.fonts.ready;
});
const fontAvailable = await page.evaluate(({ fontFamily, sample }) =>
document.fonts.check(`16px "${fontFamily}"`, sample),
{ fontFamily, sample }
);
const details = await page.evaluate(() => {
const heading = document.querySelector('h1');
return {
fontStatus: document.fonts.status,
headingFamily: heading ? getComputedStyle(heading).fontFamily : null,
headingText: heading?.textContent?.trim() ?? null
};
});
console.log({ fontAvailable, ...details });
if (!fontAvailable) {
throw new Error(`Expected font "${fontFamily}" is not available for the sample text`);
}
await page.screenshot({ path: 'landing-page.png', fullPage: true });
} finally {
await browser.close();
}
}
capture().catch(error => {
console.error(error);
process.exitCode = 1;
});
Install the standard puppeteer package with npm install puppeteer and run the script with node capture.js. The standard package downloads a compatible Chrome for Testing browser. If your project uses puppeteer-core, you must manage or connect to a browser separately; confirm which binary your job launches. See the Puppeteer installation guide.
document.fonts.ready means the document’s font loading set has settled; it is not proof that a particular external font file loaded successfully or that the element uses it. Interpret the check alongside computed styles and font request results. The font readiness mechanism is also referenced by Puppeteer’s PDF option documentation, which says Page.pdf() waits for fonts by default; that PDF behavior does not establish an automatic font wait for Page.screenshot(). See PDFOptions and FontFaceSet.ready.
2. Diagnose which font problem you have
Use the screenshot, browser diagnostics, and deployment details to distinguish timing from delivery and local font coverage. The correct fix depends on which evidence you find.
| Likely branch | Clues | Checks and next step |
|---|---|---|
| Font was late | The result changes when capture timing changes; font requests eventually succeed. | Await document.fonts.ready after navigation. If the page swaps fonts after that through later application behavior, wait for the relevant UI state or selector too. |
| Font delivery failed | A font request fails, is blocked, or returns an unsuccessful response; the page uses a fallback. | Inspect request URL, status, browser console, and deployment network access. Check that the stylesheet points to the expected asset and that the asset can be fetched in the screenshot environment. |
| Font loaded but is not applied | The font request succeeds, but computed styles show another family or a different weight/style. | Inspect the target element’s computed font-family, weight, style, and CSS rules. Confirm the font-face declaration covers the requested weight and style and that the intended selector wins. |
| Missing system font or glyph coverage | Only certain characters or scripts are absent or substituted, often in a minimal Linux image. | Check fonts installed in the container and add packages covering the required script. Puppeteer’s troubleshooting documentation specifically notes extra font files may be needed for Chinese, Japanese, and Korean characters. |
| Different browser environment | Local output is correct but CI or production output differs. | Record Puppeteer and browser versions, executable path, operating system/container image, and installed fonts. Align the browser setup and font packages. |
Check the element’s actual computed font
A CSS family list can contain several fallbacks, and a computed value may show the declared list rather than identifying the face used for each glyph. Use it as a clue, not conclusive proof of glyph-level rendering. Check the font request and compare a representative screenshot or text sample as well.
const style = await page.$eval('h1', element => {
const css = getComputedStyle(element);
return {
family: css.fontFamily,
weight: css.fontWeight,
style: css.fontStyle,
text: element.textContent
};
});
console.log(style);
Check a specific font and text sample
Use the same family name and representative characters that the page needs. A font check can help identify availability for the supplied text, but it does not establish that every element uses that face. Compare the result with the page’s CSS and request logs.
const available = await page.evaluate(() =>
document.fonts.check('700 32px "Your Web Font"', 'Welcome — 東京')
);
console.log({ available });
3. Make the wait match the page
Choose a navigation condition deliberately
waitUntil: 'networkidle2' can be useful when a page’s network activity settles, and Puppeteer’s screenshot guide uses it in an example. Page.waitForNetworkIdle() resolves after network activity is idle for its configured interval. Neither is documented as a check that the intended web font rendered. Pages with long-lived connections may also make network-idle waiting unsuitable. Treat it as a network signal, then wait for fonts separately.
For pages that render the relevant content after client-side work, wait for a selector or other application-specific ready state before checking fonts:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('h1[data-rendered="true"]', { timeout: 20000 });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'landing-page.png', fullPage: true });
Use a selector that genuinely indicates the page content is ready. A fixed delay can mask a race on one run and fail on another; prefer an observable page state and the font readiness promise.
Ensure the sample matches the font face
Check the exact family name used in the page’s @font-face or CSS, and include characters that matter. If the page requests a particular weight, use that weight in the font specification. A mismatch between the check and the CSS can produce a misleading diagnosis.
4. Check Linux and browser setup
Puppeteer documents that Linux dependencies are needed and that additional font files may be required for some character sets. A correct CSS declaration and reachable web-font URL do not provide glyphs that are absent from the runtime when the page depends on local/system fonts or fallback coverage. Inspect the actual container image, installed fonts, and launch configuration. See Puppeteer troubleshooting.
- Log the Puppeteer version, browser version, executable path, and runtime image used by the screenshot job.
- Confirm whether you use
puppeteerwith its downloaded browser orpuppeteer-corewith a separately managed browser. - Check the font files installed in the runtime and verify coverage for the scripts and symbols on the page.
- Install appropriate font packages in the image build when required, then rebuild and deploy that image.
- Capture the same URL with the same browser and runtime configuration to see whether the issue follows the environment.
Do not assume that adding system fonts will repair a failed remote font request. Diagnose delivery and local glyph coverage as separate branches.
5. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
networkidle2 completes but the screenshot has fallback text |
Network idleness is not a documented font-readiness guarantee. | Await document.fonts.ready, then check the expected face and its request. |
document.fonts.ready resolves but the expected typeface is absent |
The set settled, but the specific font may have failed, been blocked, or not been requested by the page. | Inspect font responses and console/request failures; verify the URL, CSS declaration, and deployment access. |
| The availability check returns false | The family string, weight, or sample may not match the declared face; or the face is unavailable. | Use the exact CSS family and representative text, then inspect font requests and declarations. |
| Only CJK characters or symbols are missing | The runtime may lack fonts with the needed glyph coverage. | Install suitable font files in the Linux image and verify the required script is covered. |
| Fonts work locally but not in CI | Different browser binaries, versions, operating systems, or installed fonts. | Compare the deployed browser path, Puppeteer/browser versions, image, and font packages. |
| Navigation or font wait times out | The page may have slow or continuous requests, unreachable assets, or an unsuitable readiness condition. | Use an appropriate navigation condition and page-specific selector; collect request failures. Set timeouts to match the job’s needs rather than removing them. |
| The screenshot clips content or misses below-the-fold sections | The capture dimensions or lazy-loaded content state may differ from expectations. | Use fullPage: true for a full-page capture and wait for the page’s relevant content to render before capture. |
6. Performance and reliability
- Wait for evidence, not arbitrary time. A font readiness promise and page-specific selector avoid guessing with a large fixed delay. Network-idle waits can add time and may not suit pages with persistent connections.
- Keep diagnostics in CI. Log failed requests, unsuccessful font responses, browser version, executable path, and computed style when a capture fails. This makes environment-specific differences easier to compare.
- Fail clearly when the font is required. If brand or layout fidelity depends on a specific face, treat an unavailable-font check as a capture error. If fallback is acceptable, record the diagnostic and continue intentionally.
- Use repeatable browser setup. Pin and record the browser/runtime configuration your deployment actually uses, and include necessary font packages in the built image.
- Separate image and PDF expectations. Puppeteer documents a font wait for PDF generation; do not infer that screenshot capture has the same automatic behavior.
7. Cost and SEO scope
Running Puppeteer yourself means maintaining the browser runtime, dependencies, font files, and capture code in the environment where jobs run. The time and infrastructure cost depends on your deployment; the supplied documentation does not establish a universal cost or performance figure.
This is a rendering-fidelity fix for screenshots of SEO landing pages. The available evidence does not establish that a screenshot font issue changes search rankings or indexing, so this guide makes no ranking claim.
Or skip the browser setup
If you need a clean screenshot without maintaining Puppeteer’s browser and font environment, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. Its capture flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. Its MCP server provides screenshot, page-info, and PDF tools for AI agents.
Use the API from the language you already use. See the ScreenshotNeo API documentation for options and configuration.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/landing-page -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/landing-page"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/landing-page'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = require('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for ScreenshotNeo and get 1,000 free screenshots a month, no card required.
FAQ
Does Puppeteer automatically wait for fonts before page.screenshot()?
The cited screenshot documentation does not promise that. Explicitly await document.fonts.ready and check the expected face when it matters.
Does waiting for fonts prove the intended font loaded?
No. It indicates that the document’s font loading set settled. Check the specific font, request results, styles, and representative text.
Why does the PDF look right while the screenshot does not?
Puppeteer documents that Page.pdf() waits for fonts by default. That documented PDF behavior is separate from screenshot behavior.
Can missing fonts affect SEO rankings?
The evidence covered here concerns screenshot rendering and does not establish ranking or indexing effects.


