Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsUse CSS @font-face in the HTML produced by Handlebars, make the font bytes reachable to Chromium (or embed them as Base64), apply the matching family, and create the PDF only after the page has loaded its styles and fonts. Handlebars only substitutes data into an HTML string; it does not load fonts. Puppeteer’s page.pdf() uses the browser’s print rendering and, in current releases, waits for document.fonts.ready by default.
The complete rendering flow
A reliable pipeline has four separate stages:
- Compile the Handlebars template with your data.
- Return a complete HTML document containing the font declaration and the elements that use it.
- Give Chromium a URL or data URL from which it can retrieve the font bytes.
- Generate the PDF after the page’s styles and content are present.
A server-side file path such as /app/fonts/ReportSans.woff2 is not automatically a browser URL. Use an absolute HTTP(S) URL, a suitable local URL exposed to the page, or a data: URL containing the font bytes.
| # | Preview | Product | Price | |
|---|---|---|---|---|
| 1 |
|
Instant Handlebars.js | $25.99 | Buy on Amazon |
| 2 |
|
Quick Handlebar Templating | $12.00 | Buy on Amazon |
Working Handlebars and Puppeteer example
The following pattern compiles a template, sets it as the page content, makes the font available from a web address, and writes an A4 PDF. Replace the asset URL and data with values appropriate for your deployment, and confirm that your font license permits this use.
const puppeteer = require('puppeteer');
const Handlebars = require('handlebars');
const templateSource = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
@font-face {
font-family: 'ReportSans';
src: url('https://assets.example.com/fonts/report-sans.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: block;
}
@font-face {
font-family: 'ReportSans';
src: url('https://assets.example.com/fonts/report-sans-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: block;
}
body {
font-family: 'ReportSans', sans-serif;
font-weight: 400;
}
h1, strong { font-weight: 700; }
</style>
</head>
<body>
<h1>{{title}}</h1>
<p>{{body}}</p>
</body>
</html>`;
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
const html = Handlebars.compile(templateSource)({
title: 'Quarterly report',
body: 'Prepared for the finance team.'
});
await page.setContent(html);
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
waitForFonts: true
});
} finally {
await browser.close();
}
})();
The URL in this sample is illustrative. In production, it must resolve from the Chromium process that renders the page. The font-family, font-weight, and font-style values in the rule must match the values requested by your document. If a paragraph asks for weight 700 but only a 400 file is declared, the browser may synthesize a bold face or fall back to another font.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Choosing how to supply the font
| Method | Best fit | Trade-off |
|---|---|---|
Remote URL in @font-face |
The font is already hosted and the rendering environment has network access | Validate the URL, access controls, TLS, and origin policy from the Chromium runtime |
Base64 data: URL |
A self-contained HTML document or restricted network environment | Increases HTML size and must comply with the font license |
page.addStyleTag() |
The page already exists and CSS is assembled programmatically | Inject it before capture and target the intended page |
| Installed system font | A controlled container or VM where the same font is always installed | Environment-specific; a different image can silently change the result |
Embedding a Base64 font
Read the font file in Node.js, convert it to Base64, and interpolate the result into the style block. This avoids a network fetch but can make every generated HTML string large.
const fs = require('node:fs');
const font64 = fs.readFileSync('./fonts/report-sans.woff2').toString('base64');
const template = `<style>
@font-face {
font-family: 'ReportSans';
src: url(data:font/woff2;base64,${font64}) format('woff2');
font-weight: 400;
font-style: normal;
}
body { font-family: 'ReportSans', sans-serif; }
</style>`;
Use the MIME type that matches the file format, and do not embed a font when redistribution is prohibited by its license.
Injecting CSS after navigation
await page.setContent(html);
await page.addStyleTag({
content: `
@font-face {
font-family: 'ReportSans';
src: url('https://assets.example.com/fonts/report-sans.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
body { font-family: 'ReportSans', sans-serif; }
`
});
await page.pdf({ path: 'report.pdf', waitForFonts: true });
Injection is useful when a shared template is already loaded, but it does not solve an unreachable URL. The browser still has to fetch the bytes.
Waiting for fonts before creating the PDF
Current Puppeteer PDF options include waitForFonts, whose default is true; it waits for document.fonts.ready. Keeping the option explicit makes the intent clear:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
await page.pdf({
path: 'report.pdf',
format: 'A4',
waitForFonts: true
});
For a diagnostic or a render sequence with additional asynchronous work, you can make the wait visible yourself:
await page.evaluate(() => document.fonts.ready);
await page.pdf({ path: 'report.pdf', waitForFonts: true });
The explicit wait is not normally a required workaround. If the page is running in the background, activate it with page.bringToFront() before waiting. Also wait for your own application work—such as data-driven DOM updates—before asking the browser to create the PDF.
Print CSS changes what appears in the PDF
page.pdf() renders with the print CSS media type. A font that appears correct in a screen preview can therefore be changed by an @media print rule, a print-only stylesheet, or a different font declaration later in the cascade.
@media print {
body { font-family: 'ReportSans', sans-serif; }
.screen-only { display: none; }
}
When diagnosing a mismatch, inspect the generated PDF path, not only the page in a normal screen-media preview. Use browser developer tools or evaluate the computed style on the element that is visibly wrong.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Debugging a font that does not appear
1. Inspect the compiled HTML
Log or save the exact string returned by Handlebars.compile(...). Confirm that the <style> element survived compilation, the family spelling is identical everywhere, and the target element actually receives that family. Handlebars performs substitution; it is not a font loader.
2. Verify reachability from Chromium
Open the font URL from the same runtime that launches Chromium. Check DNS, TLS, authentication, redirects, and response status. A path that exists on the Node.js host can still be unavailable to the browser. Switch to an absolute reachable URL or a Base64 data URL when appropriate.
3. Check the descriptors
- Declare every weight and style you use.
- Use the correct format string, such as
format('woff2'). - Keep a generic fallback such as
sans-serif. - Look for a later selector that overrides
font-family.
4. Check the network response
In a diagnostic run, listen for failed requests and inspect the response for the font URL. A 404, an authentication redirect, or a server that returns HTML instead of font bytes will leave the browser using a fallback. Fix the server response rather than adding more waits.
Rank #2
5. Check print-only rules
Remove or correct any print rule that replaces the family. Compare the computed style under print media and screen media if the two views differ.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minute6. Wait after the final DOM and CSS change
Call document.fonts.ready only after Handlebars content, injected styles, and any client-side changes are complete. Calling it before adding the @font-face rule cannot wait for a font that did not yet exist.
7. Treat headers and footers separately
PDF header and footer values are separate template options. Do not assume a body font declaration automatically applies to those templates. Test them independently with the Puppeteer version deployed by your application.
Common symptoms, causes, and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Everything uses a system font | The URL cannot be fetched or the family name does not match | Check the compiled HTML and Chromium’s request; use an absolute URL or data URL |
| Regular text works but bold does not | No matching 700 declaration or the file is mapped to the wrong weight | Add the correct @font-face rule and apply font-weight: 700 |
| Screen view is correct, PDF is not | Print media rules change the family | Inspect and correct @media print CSS |
| Intermittent fallback fonts | Capture starts before asynchronous content or font CSS is complete | Finish DOM work, await document.fonts.ready, and keep waitForFonts enabled |
| Body is correct but header/footer is not | Those templates have separate rendering behavior | Declare and verify their font styling independently |
| Base64 version is slow or oversized | The font is embedded repeatedly in large HTML strings | Use a reachable hosted asset when permitted, or cache the compiled CSS/data carefully |
Performance, reliability, and licensing considerations
- Prefer WOFF2: it generally transfers less data than older web-font formats, provided your target Chromium build supports the file.
- Cache immutable assets: a versioned font URL avoids repeated downloads while making updates explicit.
- Limit the faces you declare: loading regular, bold, italic, and multiple language subsets increases work even when the document uses only one face.
- Make failures visible: retain a generic fallback and log failed font requests so a missing custom face does not produce an unreadable PDF.
- Keep the rendering image consistent: system-font fallback and Chromium versions can change line breaks, pagination, and widows/orphans.
- Respect the license: hosting or embedding a font can have different redistribution terms from desktop use.
Or skip the browser setup
If your goal is to capture a rendered page rather than maintain a Puppeteer rendering service, ScreenshotNeo provides a website screenshot API and MCP server. A single request returns PNG, JPEG, WebP, or PDF; it can wait for a selector, delay, or network idle and supports custom CSS and JavaScript when your page needs final rendering adjustments.
For a direct capture, use the API documented at https://screenshotneo.com/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}`);
Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 shots; all features are included on every plan. Create a free ScreenshotNeo account.
FAQ
Can Handlebars itself load a font?
No. It produces an HTML string. Chromium resolves the CSS and fetches or decodes the font when Puppeteer loads that string.
Is waitForFonts: true still needed if I call document.fonts.ready?
The PDF option already defaults to waiting. Keeping both can document the sequence during troubleshooting, but the explicit JavaScript wait is not inherently required twice.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Why does a local relative URL work in a browser but fail in a PDF job?
The browser you tested may have a different base URL and filesystem access. page.setContent() receives an HTML string, so make the font source resolvable from the Chromium page or embed it.
Can I use the same font for PDF headers and footers?
Headers and footers are separate PDF template options. Verify custom-font behavior for those templates in the exact Puppeteer and Chromium versions you deploy instead of assuming body CSS carries over.
Quick Recap
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.

