santifer/career-ops · warning

⚠️ Font file not found, keeping original reference: fonts/$

Error message

⚠️  Font file not found, keeping original reference: fonts/${name}

What it means

Graceful-degradation warning while inlining fonts for PDF rendering in generate-pdf.mjs: a CSS font reference matching fonts/<name> could not be read with ENOENT, so instead of embedding a data: URL the original 'fonts/<name>' reference is kept and Chromium falls back to system fonts. Non-ENOENT errors still propagate; only a genuinely missing file warns.

Source

Thrown at generate-pdf.mjs:1409

    const fontPath = resolve(fontsDir, name);
    const rel = relative(fontsDir, fontPath);
    if (rel.startsWith('..') || isAbsolute(rel)) {
      console.warn(`⚠️  Font reference escapes fonts/, keeping original reference: ${name}`);
      continue;
    }
    if (_fontDataUrlCache.has(fontPath)) {
      dataUrls.set(name, _fontDataUrlCache.get(fontPath));
      continue;
    }
    try {
      const buf = await readFile(fontPath);
      const ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase();
      const dataUrl = `url('data:${MIME[ext] || 'application/octet-stream'};base64,${buf.toString('base64')}')`;
      _fontDataUrlCache.set(fontPath, dataUrl);
      dataUrls.set(name, dataUrl);
    } catch (err) {
      if (err?.code !== 'ENOENT') throw err;
      console.warn(`⚠️  Font file not found, keeping original reference: fonts/${name}`);
    }
  }
  return html.replace(FONT_REF, (match, _quote, name) => dataUrls.get(name) || match);
}

/**
 * Render an HTML string to a PDF file via headless Chromium.
 *
 * Writes the HTML to a temporary file in the baseDir and loads it via
 * page.goto() to give the page a file:// origin. This allows relative
 * resources (images, fonts) to load — setContent() runs from about:blank
 * and Chromium blocks file:// subresource loads from non-file origins.
 *
 * Local url('./fonts/...') references are inlined as data: URLs first so
 * fonts also survive the ATS normalization pass (which may strip font refs).
 *
 * @param {string} html - Full HTML document to render.
 * @param {string} outputPath - Absolute path to write the PDF to.

View on GitHub (pinned to 60398d6549)

Solutions

  1. Put the missing font file at fonts/<name> relative to the render baseDir (exact name from the warning)
  2. Or remove/replace the @font-face reference in the template if the font is not needed
  3. Verify the PDF afterwards — layout metrics (page count, spacing) can shift under fallback fonts

Example fix

# before
cp template-with-fonts.html my-cv.html && node generate-pdf.mjs my-cv.html   # fonts/Inter.ttf absent
# after
mkdir -p fonts && cp ~/Downloads/Inter.ttf fonts/Inter.ttf && node generate-pdf.mjs my-cv.html
Defensive patterns

Strategy: fallback

Validate before calling

import { existsSync } from 'node:fs';
import { join } from 'node:path';
for (const name of fontNamesReferencedBy(html)) {
  if (!existsSync(join(baseDir, 'fonts', name))) {
    console.warn(`font missing, expect fallback metrics: fonts/${name}`);
  }
}

Type guard

function hasAllFonts(html, baseDir) {
  const names = [...html.matchAll(FONT_REF)].map((m) => m[2]);
  return names.every((n) => existsSync(join(baseDir, 'fonts', n)));
}

Try / catch

// Already warn-and-fallback by design; callers should react to the warning:
if (!hasAllFonts(html, baseDir)) {
  // either populate fonts/ or accept layout drift and re-check pageCount
  console.warn('font fallback will be used; verify page count and spacing');
}

Prevention

When it happens

Trigger: Rendering a CV whose template references fonts/<name> while the fonts/ directory (resolved against baseDir) lacks that file — e.g. template updated to a new font family, fonts/ never populated, or rendering from a cwd/baseDir where fonts/ does not exist. _fontDataUrlCache means the miss is retried per process, not cached.

Common situations: Customized templates/cv-template.html referencing webfonts never downloaded; cloning without the (gitignored) fonts/ dir; renaming a font file but not the CSS; batch renders from a different baseDir than interactive ones.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/5d258976956e2fe9. Report an issue: GitHub.