Skip to content

Custom fonts are never embedded in PDFs - setContent() leaves the page at about:blank, Chromium blocks all file:// font loads #951

@shuhia

Description

@shuhia

Code of Conduct

Existing issues

  • I searched existing issues and this hasn't been reported yet

What happened?

Custom fonts never load in generate-pdf.mjs — every generated PDF silently falls back to system fonts. The template's brand typography (Space Grotesk / DM Sans from fonts/*.woff2) is never embedded; on Windows the PDFs embed only Arial. This is platform-independent (Chromium security policy, not an OS quirk) and completely silent: document.fonts.ready resolves anyway, the PDF generates fine, exit code 0.

Found while validating #949 — that PR makes the injected file:// font URLs valid on Windows, but fonts still don't load even with valid URLs, because of a second, independent problem:

Root cause: renderHtmlToPdf() loads the document with page.setContent(html, { baseURL: 'file://...' }). After setContent, the page's actual URL remains about:blankbaseURL only affects relative-URL resolution, not the document origin. Chromium refuses to load local files from a non-file:// page, so every file:///...woff2 request is blocked:

CONSOLE: error Not allowed to load local resource: file:///.../fonts/dm-sans-latin.woff2
REQ FAILED: file:///.../fonts/dm-sans-latin.woff2 -- other
FONTS: [{"family":"DM Sans","status":"error"}]
PAGE URL: about:blank

(Minimal Playwright repro: setContent an HTML with one @font-face pointing at an absolute file:/// woff2 URL, listen to console/requestfailed, inspect document.fonts statuses.)

Checking the fonts actually embedded in a PDF produced from templates/cv-template.html confirms it — only the fallback:

/BaseFont names: AAAAAA+Arial-BoldMT, BAAAAA+ArialMT     # no SpaceGrotesk, no DMSans

Steps to reproduce

  1. node generate-pdf.mjs templates/cv-template.html output/test.pdf (on any OS, with fix(generate-pdf): silent no-op on Windows - build file:// URLs with pathToFileURL #949 applied or not)
  2. Extract /BaseFont names from the PDF (or open it) → system fallback fonts, not Space Grotesk / DM Sans.

Expected behavior

The woff2 fonts shipped in fonts/ are loaded and embedded in the PDF.

Verified fix direction

Same machine, same font, same Chromium — navigating to a real file:// page instead of setContent loads the font:

FONTS: [{"family":"DM Sans","status":"loaded"}]
PAGE URL: file:///.../output/font-debug2.html

Options, roughly in order of robustness:

  1. Inline the fonts as base64 data: URLs during the font injection step in generatePDF() — no origin/security dependency at all, works with setContent unchanged. ~5 lines: read each woff2, replace url('./fonts/X.woff2') with url('data:font/woff2;base64,...').
  2. Write the final HTML to a temp file and page.goto(pathToFileURL(tmp).href) — document origin becomes file://, local subresources load; delete the temp file after rendering.
  3. page.route() interception serving the font bytes.

Happy to send a PR once there's a preferred direction (I'd suggest option 1: it also keeps renderHtmlToPdf()'s API unchanged for other callers).

CLI tool

Claude Code

OS and Node.js version

Windows 11 / Node v24.14.0, Playwright 1.58.1 (Chromium headless shell) — but the blocking behavior is Chromium policy and applies on Linux/macOS as well.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions