# How the PDF Generation Pipeline Converts HTML to ATS‑Parseable PDFs in Career‑Ops

> Learn how the Career Ops PDF generation pipeline converts HTML to ATS-parseable PDFs. Discover the Node.js script, headless Chromium rendering, and Playwright integration for clean, searchable documents.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: internals
- Published: 2026-06-13

---

**The PDF generation pipeline in `santifer/career-ops` uses a Node.js script to sanitize HTML for ATS compatibility, resolve local font paths to absolute URLs, and render the final document via headless Chromium using Playwright, producing clean, searchable PDFs that pass through applicant tracking systems without character mangling.**

The `career-ops` repository by `santifer` provides a robust toolkit for generating job application materials. At its core, the **PDF generation pipeline** transforms styled HTML templates into professional, ATS‑friendly documents through a carefully orchestrated sequence of sanitization and rendering steps.

## Pipeline Stages Explained

### CLI and Programmatic Entry Points

The pipeline begins in `generate-pdf.mjs`, which supports both command‑line and module invocation. When run from the terminal, it accepts positional arguments for input HTML and output PDF paths, plus an optional `--format` flag (values: `letter` or `a4`). The entry point validates these parameters, resolves absolute paths for both input and output files, and prepares the environment for processing.

### HTML Loading and CV Validation

The script reads the HTML file using `fs/promises.readFile`. If a [`cv.md`](https://github.com/santifer/career-ops/blob/main/cv.md) file exists alongside the template, the pipeline loads it to verify section ordering through `validateCvSectionOrder`. This check ensures the rendered PDF maintains the same structure as the source markdown, preventing accidental reordering during conversion.

### Font URL Resolution

To ensure Chromium can access typography regardless of where the HTML is temporarily stored, the pipeline rewrites all relative `url('./fonts/...')` references inside the HTML to absolute `file://` URLs pointing to the `fonts/` directory adjacent to the script. This guarantees font availability even when the HTML is opened from a temporary location.

### ATS‑Friendly Text Normalisation

Before rendering, `normalizeTextForATS` scans the HTML body for typographic Unicode characters—including smart quotes, em‑dashes, en‑dashes, non‑breaking spaces, zero‑width characters, arrows, bullets, and currency symbols—and replaces them with plain ASCII equivalents. This step is critical because many ATS parsers fail when encountering these special characters, resulting in mangled or unreadable text. The function logs a detailed map of replacements for debugging purposes.

### Headless Rendering with Playwright

The `renderHtmlToPdf` function launches a headless Chromium instance via Playwright, creates a new page, and injects the processed HTML using `page.setContent` with a `baseURL` option pointing to the original HTML directory. It waits for `document.fonts.ready` to ensure all web fonts load before generating the PDF. The output uses the specified paper size with printable backgrounds enabled and 0.6‑inch margins on all sides.

### Output Generation and Metadata

After rendering, the PDF buffer is written to the target path. The script analyzes the PDF structure to count pages, logs the file size, and returns an object containing `outputPath`, `pageCount`, and byte size.

## Usage Examples

### Command‑Line Usage

Generate a PDF from the command line by specifying the input HTML and output paths:

```bash
node generate-pdf.mjs templates/cv-template.html output/my-cv.pdf --format=letter

```

### Programmatic Integration

Import the rendering function into your own scripts for custom workflows:

```javascript
import { readFile } from "fs/promises";
import { renderHtmlToPdf } from "./generate-pdf.mjs";

async function makePdf() {
  const html = await readFile("templates/cv-template.html", "utf-8");
  const result = await renderHtmlToPdf(html, "output/cv.pdf", { format: "a4" });
  console.log(`PDF written to ${result.outputPath}, ${result.pageCount} pages`);
}
makePdf();

```

### Cover Letter Generation

The same pipeline powers the cover letter generator, ensuring consistent output:

```bash
node generate-cover-letter.mjs --payload data/cover-letter.json

```

## Re‑using the Pipeline for Cover Letters

The same rendering engine powers the cover letter generator in `generate-cover-letter.mjs`. This script constructs an HTML payload from a JSON template, then calls `renderHtmlToPdf` identically to the CV pipeline, ensuring consistent ATS compatibility and formatting across both CVs and cover letters.

## Summary

- The pipeline is orchestrated by `generate-pdf.mjs`, which handles both CLI and programmatic workflows.
- **ATS text normalisation** via `normalizeTextForATS` converts Unicode typographic characters to ASCII to prevent parsing errors.
- Font resources are resolved to absolute `file://` paths to ensure portability across different execution environments.
- Rendering uses Playwright with headless Chromium, waiting for font loading before PDF generation.
- The same `renderHtmlToPdf` function is reused by `generate-cover-letter.mjs` for consistent output across document types.

## Frequently Asked Questions

### What is ATS‑friendly text normalisation and why does it matter?

ATS‑friendly text normalisation is the process of converting typographic Unicode characters—such as smart quotes, em‑dashes, and special symbols—into plain ASCII equivalents. Most applicant tracking systems use older parsing engines that cannot interpret these Unicode characters, often resulting in garbled text or complete parsing failures. The `normalizeTextForATS` function in `generate-pdf.mjs` ensures the final PDF contains only characters that ATS software can reliably read.

### Can I use the PDF generator with custom HTML templates?

Yes. The pipeline accepts any valid HTML file as input. You can invoke it programmatically by importing `renderHtmlToPdf` from `generate-pdf.mjs` and passing your HTML string, output path, and format options (A4 or Letter). Ensure your template references fonts using relative paths (`./fonts/...`), as the script automatically rewrites these to absolute `file://` URLs during processing.

### How does the pipeline ensure fonts render correctly in the final PDF?

The script resolves all local font references to absolute `file://` paths before rendering. It then uses Playwright to launch Chromium and waits for `document.fonts.ready` to fire before generating the PDF, guaranteeing that all web fonts are fully loaded and embedded in the output document.

### Is the PDF generation process the same for CVs and cover letters?

Yes. Both document types use the identical `renderHtmlToPdf` function from `generate-pdf.mjs`. The cover letter generator (`generate-cover-letter.mjs`) constructs its HTML from a JSON payload, but delegates the actual rendering, ATS sanitisation, and PDF writing to the same core function used for CVs, ensuring consistent formatting and compatibility.