ScreenshotNeo

BlogHTML to image & PDF

How to Fix Google Fonts Not Loading in Puppeteer PDFs

Fix missing Google Fonts in Puppeteer PDFs by checking both requests, CSS weights, print media, and document.fonts readiness.

By the ScreenshotNeo team1 October 20267 min read

When Google Fonts are missing from a Puppeteer PDF, check the font stylesheet request and the subsequent font-file request in the same browser environment that creates the PDF. Then verify the family and weights used by your CSS, wait for the documented font-readiness condition, and inspect print styles. A page can look correct in a normal browser while the PDF process cannot reach the font host or applies different print CSS.

Puppeteer’s current PDF options document waitForFonts: true by default; it waits for document.fonts.ready. If the page is in the background, the documentation notes that page.bringToFront() may be required. See the PDFOptions reference.

1. Reproduce the PDF in the real rendering environment

Record the Puppeteer version, Chromium version, container or host, proxy settings, and the URL being rendered. Run the same code in CI or the production container, because a laptop with outbound access can hide a blocked request in production.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: true,
  // Supply this only when your environment requires a proxy:
  // args: ['--proxy-server=http://proxy.example:8080']
});
const page = await browser.newPage();
await page.goto('https://example.com/invoice', { waitUntil: 'networkidle2' });
await page.pdf({ path: 'invoice.pdf', format: 'A4', printBackground: true });
await browser.close();

Puppeteer’s PDF guide uses navigation waiting and documents that PDF generation waits for fonts. Treat networkidle2 as a page-load aid, not proof that a font loaded.

2. Inspect both Google Fonts network stages

Google’s delivery flow has two relevant requests: a stylesheet from the Fonts CSS API, then one or more font files selected for the requesting browser. A successful stylesheet response does not prove that a font file downloaded. See Google’s getting-started documentation.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
const failures = [];
const responses = [];

page.on('requestfailed', request => {
  failures.push({ url: request.url(), error: request.failure()?.errorText });
});
page.on('response', response => {
  const url = response.url();
  if (url.includes('fonts.googleapis.com') || url.includes('fonts.gstatic.com')) {
    responses.push({ url, status: response.status(), ok: response.ok() });
  }
});
page.on('console', message => console.log('[browser]', message.type(), message.text()));
page.on('pageerror', error => console.error('[pageerror]', error));

await page.goto('https://example.com/invoice', { waitUntil: 'networkidle2' });
console.log(JSON.stringify({ responses, failures }, null, 2));
await browser.close();

Look for redirects, blocked requests, DNS or TLS failures, non-2xx statuses, and Content Security Policy errors. A stylesheet may reference a font file on a different host, so check both hostnames. Also check whether a service worker, request interception rule, ad blocker, or corporate proxy rewrites the request.

3. Confirm the family, style, and weight actually used

Google’s setup pattern links a stylesheet in the document and applies the family in CSS. Make the names and weights match exactly, including italic versus normal. Keep a generic fallback for graceful failure, but do not treat readable fallback text as evidence that the requested font loaded.

<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">
<style>
  body {
    font-family: "Roboto", Arial, sans-serif;
    font-weight: 400;
  }
  h1 { font-weight: 700; }
</style>

Inspect the computed style and the browser’s font faces before creating the PDF:

const fontState = await page.evaluate(() => ({
  bodyFont: getComputedStyle(document.body).fontFamily,
  bodyWeight: getComputedStyle(document.body).fontWeight,
  status: document.fonts.status,
  faces: [...document.fonts].map(face => ({
    family: face.family, style: face.style, weight: face.weight, status: face.status
  }))
}));
console.log(fontState);

If the required face is absent or remains unloaded/error, fix the URL, CSS, network access, or requested weight before changing timing.

4. Wait for fonts using the documented condition

page.pdf() waits for fonts by default in current Puppeteer. You can make the intent explicit and add a bounded diagnostic wait for a known family:

await page.bringToFront();
await page.evaluate(async () => {
  await document.fonts.ready;
  await document.fonts.load('700 16px "Roboto"');
});

await page.pdf({
  path: 'invoice.pdf',
  format: 'A4',
  printBackground: true,
  waitForFonts: true
});

A custom wait should be bounded so a broken font host cannot hang the job:

async function waitForFont(page, descriptor, timeoutMs = 10000) {
  const ready = page.evaluate(async d => {
    await document.fonts.ready;
    return document.fonts.check(d);
  }, descriptor);
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Font wait timed out: ${descriptor}`)), timeoutMs)
  );
  const loaded = await Promise.race([ready, timeout]);
  if (!loaded) throw new Error(`Font is not available: ${descriptor}`);
}

await waitForFont(page, '700 16px "Roboto"');

This bounded check is an implementation pattern, not a Puppeteer guarantee. Log the descriptor and failed URLs when it fails.

5. Check print media and PDF-specific CSS

Puppeteer PDFs render with the print media type. An @media print rule can replace the screen family, weight, or size. Compare the computed style under print media, and use screen emulation only as a diagnostic when the design is intended to match the screen.

await page.emulateMediaType('print');
console.log(await page.evaluate(() => ({
  family: getComputedStyle(document.body).fontFamily,
  weight: getComputedStyle(document.body).fontWeight
})));

// Diagnostic comparison only:
await page.emulateMediaType('screen');
console.log(await page.evaluate(() => getComputedStyle(document.body).fontFamily));

Search all stylesheets for print overrides, inherited font-family, and weights that were never requested from Google. Header and footer templates have their own CSS and can be a separate case; test them independently.

6. A complete diagnostic-to-PDF script

import puppeteer from 'puppeteer';

const target = process.argv[2] || 'https://example.com/invoice';
const family = 'Roboto';
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const fontRequests = [];
const failed = [];

page.on('response', r => {
  if (/fonts\.(googleapis|gstatic)\.com/.test(r.url())) {
    fontRequests.push({ url: r.url(), status: r.status(), ok: r.ok() });
  }
});
page.on('requestfailed', r => failed.push({ url: r.url(), error: r.failure()?.errorText }));
page.on('console', m => console.log(`[console:${m.type()}] ${m.text()}`));
page.on('pageerror', e => console.error('[pageerror]', e));

try {
  await page.goto(target, { waitUntil: 'networkidle2', timeout: 30000 });
  await page.bringToFront();
  await page.evaluate(() => document.fonts.ready);
  const state = await page.evaluate(f => ({
    status: document.fonts.status,
    check400: document.fonts.check(`400 16px "${f}"`),
    check700: document.fonts.check(`700 16px "${f}"`),
    family: getComputedStyle(document.body).fontFamily
  }), family);
  console.log(JSON.stringify({ fontRequests, failed, state }, null, 2));
  await page.pdf({ path: 'output.pdf', format: 'A4', printBackground: true, waitForFonts: true });
} finally {
  await browser.close();
}

Common failures and fixes

Symptom Likely cause Fix
Stylesheet 200, font file failed Blocked gstatic host, DNS/TLS, proxy, CSP, or egress policy Inspect the font-file URL and response; allow the host or use reachable assets.
Fallback appears only in PDF PDF environment differs from your desktop Log requests in the PDF process and compare container networking, proxy, and browser versions.
Only bold or italic is wrong That weight/style was not requested or CSS name differs Request the exact weight/style and verify computed CSS.
Screen looks right, PDF differs @media print override Inspect print computed styles; fix print CSS or use screen emulation as a diagnostic.
Adding a long sleep changes nothing The request is failing, not merely slow Check request failures and document.fonts; use a bounded wait with a useful error.
Job hangs indefinitely Unbounded navigation or font wait Set navigation and custom-wait timeouts, then report the URL and descriptor.
Fonts work locally but not in CI Different outbound access, CA certificates, or sandbox policy Test from the CI/container and fix its network trust or dependency policy.
Header/footer has a different font Template CSS and page CSS are separate Inline or explicitly load the template’s font CSS and test that path separately.

Self-hosting Google Font files

If outbound Google Fonts access is unavailable or unreliable, serving a licensed font file from infrastructure the job can reach removes that external dependency. Validate the font file, CSS URL/path, MIME type, and license. Self-hosting adds update and licensing work and does not fix a wrong family, weight, print rule, or malformed font.

Performance, reliability, and cost choices

  • Use one browser instance and reuse pages for batches; launching Chromium for every PDF adds startup cost.
  • Keep navigation and font waits bounded, and record request failures, statuses, browser errors, and the selected family/weights.
  • Request only the weights and styles the document uses. This reduces downloads without changing the diagnosis.
  • Cache stable font assets where your deployment policy permits, but invalidate the cache when CSS or font files change.
  • External fonts require outbound access and trust-store configuration. Self-hosted fonts trade that dependency for asset maintenance and licensing responsibility.
  • Do not claim a font is ready solely because text is visible; fallback rendering can conceal failure.

Or skip the browser setup

ScreenshotNeo provides a website capture API and MCP server. Its capture pipeline accepts cookie/consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

For a PDF or image capture, make one request (see the ScreenshotNeo docs):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo has 1,000 free shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Does networkidle2 guarantee Google Fonts are loaded?

No. It only describes network activity at navigation time. Inspect the stylesheet and font-file responses and check document.fonts.

Should I always set waitForFonts: false for speed?

No. The current default is true. Disable it only when you have a measured reason and another explicit readiness strategy.

Can a fallback stack prevent a PDF failure?

It keeps text readable, but it does not make the requested Google Font available. Treat fallback as resilience and instrument the requested face.

When is self-hosting justified?

Consider it when the rendering environment cannot reliably reach Google’s endpoints and you can accept font licensing and update responsibilities.