<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hogan</title>
    <description>The latest articles on DEV Community by Hogan (@hogan_44bb7725d1f6b75493b).</description>
    <link>https://dev.to/hogan_44bb7725d1f6b75493b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4231659%2F2a5abf96-1efc-4cf9-bcb7-ece90a83cdf9.png</url>
      <title>DEV Community: Hogan</title>
      <link>https://dev.to/hogan_44bb7725d1f6b75493b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hogan_44bb7725d1f6b75493b"/>
    <language>en</language>
    <item>
      <title>Solved: Puppeteer PDF uses the wrong font - it's a loading race</title>
      <dc:creator>Hogan</dc:creator>
      <pubDate>Sat, 07 Nov 2026 07:28:11 +0000</pubDate>
      <link>https://dev.to/hogan_44bb7725d1f6b75493b/solved-puppeteer-pdf-uses-the-wrong-font-its-a-loading-race-2651</link>
      <guid>https://dev.to/hogan_44bb7725d1f6b75493b/solved-puppeteer-pdf-uses-the-wrong-font-its-a-loading-race-2651</guid>
      <description>&lt;p&gt;Your browser shows the web font; the PDF ships Arial, because a print embeds whatever the layout is holding at the instant it runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; wait for the page to apply the font, then print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.font-applied&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// whatever selector appears with the new family&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluateHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;document.fonts.ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// only now does it have something to wait for&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On webcapture's API the same fix is one parameter, &lt;code&gt;settle: 1200&lt;/code&gt;; the copy-paste request is further down. The rest of this post is why the famous line fails, and why the bug sometimes refuses to happen at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that refused to reproduce
&lt;/h2&gt;

&lt;p&gt;We went looking because our own API page promises "web fonts load" and the engine's default settle window is 350 ms. If that promise leaned on luck, better we found out than a customer.&lt;/p&gt;

&lt;p&gt;Our first reproduction: &lt;code&gt;@font-face&lt;/code&gt; in the initial stylesheet, the font file delayed 800 ms on a local server, printed immediately after &lt;code&gt;load&lt;/code&gt;, the way a naive pipeline does. The web font came out in the PDF, five runs out of five. The explanation is the safe regime: Chromium holds the &lt;code&gt;load&lt;/code&gt; event until fonts the initial layout needs have arrived, even lazy 800 ms ones. A font that never loses this race cannot explain a complaint that shows up in every HTML-to-PDF engine's issue tracker, so the bug that matters needs a different trigger: a font applied by JavaScript after &lt;code&gt;load&lt;/code&gt;, the way hydration and webfont-loader pages do it. Same server, same delay, but the &lt;code&gt;@font-face&lt;/code&gt; is injected 300 ms after &lt;code&gt;load&lt;/code&gt;, with the file taking another 500 ms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Print timing&lt;/th&gt;
&lt;th&gt;Font embedded in the PDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Immediately after &lt;code&gt;load&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ArialMT (fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After naive &lt;code&gt;fonts.ready&lt;/code&gt; (resolved in 8 ms)&lt;/td&gt;
&lt;td&gt;ArialMT (fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After a 1200 ms settle&lt;/td&gt;
&lt;td&gt;the web font&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle row is the trap. &lt;code&gt;fonts.ready&lt;/code&gt; resolved 8 ms after &lt;code&gt;load&lt;/code&gt; because nothing had asked for a font yet; on a second instrumented timeline it settled at 252 ms and &lt;code&gt;loadingdone&lt;/code&gt; fired at 1029 ms. It is a promise about pending loads, not future ones. In every row &lt;code&gt;fonts.check&lt;/code&gt; came back true because the probe ran after the font had landed: the embedded font is decided at print time, not at probe time. And &lt;code&gt;fonts.check&lt;/code&gt; lies a second way: it reports true when a locally installed font shares the family name, even if the web font never loaded at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the short version works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;waitForSelector(".font-applied")&lt;/code&gt; waits for the moment the family actually switches, so &lt;code&gt;document.fonts.ready&lt;/code&gt; finally has something to wait for: the right primitive per &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready" rel="noopener noreferrer"&gt;MDN&lt;/a&gt;, but only after something has asked for the font. When you cannot touch the page, the settle window below works the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the webcapture API
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;settle&lt;/code&gt; defaults to 350 ms and accepts 0 to 10000, so the short version ships as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://webcapture.io/v1/convert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR-API-KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://example.com/report", "settle": 1200}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;waitFor&lt;/code&gt; takes a CSS selector bounded by min(settle, 10 seconds), which covers the &lt;code&gt;.font-applied&lt;/code&gt; pattern. Passing &lt;code&gt;javascript: "document.fonts.ready"&lt;/code&gt; runs before capture; it is harmless on initial-CSS pages, where &lt;code&gt;load&lt;/code&gt; already holds for fonts, and it does not help on pages that apply fonts late, where it inherits the middle-row trap.&lt;/p&gt;

&lt;p&gt;What no wait fixes: &lt;code&gt;font-display: optional&lt;/code&gt;, where the browser declines the web font for that pageview after its ~100 ms window (it still downloads and caches, so a later visit can render it). And a family name with no &lt;code&gt;@font-face&lt;/code&gt; behind it at all, which is a host font problem, not a race.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked November 2026 - the demo above re-ran on this date with the same result: print-after-load and naive &lt;code&gt;fonts.ready&lt;/code&gt; embedded ArialMT, the 1200 ms settle embedded the web font, and &lt;code&gt;document.fonts.check&lt;/code&gt; reported true in all three rows.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://webcapture.io/guides/html-to-pdf-without-losing-formatting" rel="noopener noreferrer"&gt;HTML to PDF without losing formatting&lt;/a&gt;, &lt;a href="https://webcapture.io/docs" rel="noopener noreferrer"&gt;the render options reference&lt;/a&gt;, &lt;a href="https://webcapture.io/vs/gotenberg-vs-puppeteer" rel="noopener noreferrer"&gt;Gotenberg vs Puppeteer&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>pdf</category>
      <category>puppeteer</category>
    </item>
  </channel>
</rss>
