# How Career-Ops Generates ATS-Optimized PDF CVs: A Technical Deep Dive

> Discover how Career-ops generates ATS-optimized PDF CVs. Learn about HTML sanitization, Playwright rendering, and page budget enforcement for flawless application tracking system compatibility.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: deep-dive
- Published: 2026-08-28

---

**Career-ops generates ATS-optimized PDFs by sanitizing HTML content to remove parser-breaking characters, rendering through Playwright's headless Chromium engine, and enforcing configurable page budgets with section-order validation.**

The `santifer/career-ops` repository automates the creation of applicant-tracking-system-friendly curriculum vitae through a sophisticated Node.js pipeline. This article examines the exact mechanisms that transform structured YAML profile data into machine-parseable PDF documents, focusing on the sanitization algorithms, rendering architecture, and quality enforcement systems implemented in the codebase.

## HTML Sanitization and PDF Rendering Pipeline

The core conversion logic resides in `generate-pdf.mjs`, which orchestrates the transformation from pre-rendered HTML to ATS-compatible PDF.

### Text Normalization for ATS Compatibility

Before PDF generation, candidate HTML undergoes aggressive character sanitization through the **`normalizeTextForATS`** function (lines 42-55). This utility replaces typographic characters that commonly cause ATS parsing failures with strict ASCII equivalents:

- Smart quotes (curly quotes) → straight quotes
- Em-dashes and en-dashes → hyphens
- Zero-width spaces and non-breaking spaces → standard spaces
- Unicode bullets and arrows → plain text alternatives

This normalization ensures that recruiter-facing parsing engines receive clean, expected character encodings rather than Unicode edge cases that trigger downstream ingestion errors.

### Playwright Rendering Engine

After sanitization, the cleaned HTML feeds into the **`generatePDF`** function (starting line 88), which leverages Playwright's headless Chromium browser to produce the final document. The renderer imports the `chromium` instance (line 36) to execute the conversion in a controlled browser environment, ensuring consistent typography and layout across operating systems while maintaining the semantic structure required by ATS parsers.

## Section Order Validation and Dynamic Reordering

Career-ops implements strict schema enforcement for CV structure, allowing both validation and flexible reordering based on user configuration.

### Validation Against Canonical Order

The **`validateCvSectionOrder`** function (lines 46-71) compares the rendered HTML section sequence against the canonical order defined in [`cv.md`](https://github.com/santifer/career-ops/blob/main/cv.md). When the algorithm detects a mismatch between actual and expected section placement, the pipeline aborts immediately unless the user explicitly passes the **`--allow-reorder`** flag, protecting against accidental structural corruption during automated builds.

### Custom Section Configuration

Users define preferred section sequences in [`config/profile.yml`](https://github.com/santifer/career-ops/blob/main/config/profile.yml) under the `cv.sections` key:

```yaml
cv:
  sections:
    - summary
    - skills
    - experience
    - projects
    - education

```

When `--allow-reorder` is active, the **`reorderCvSections`** function (lines 79-130) permutes the HTML DOM to match this configuration while preserving markup integrity and parent-child relationships. Running `/career-ops pdf` automatically invokes this reordering behavior.

## Page Budget Enforcement and Manifest Management

The system implements hard constraints on document length and maintains audit trails for generated artifacts.

### Page Count Constraints

After PDF creation, **`countRenderedPdfPages`** (lines 30-48) extracts the page count from the PDF catalog metadata. The **`enforcePageBudget`** function (lines 99-119) compares this count against the **`--max-pages`** parameter (defaulting to 2). Exceeding the budget triggers a warning log by default, or throws a fatal error when **`--strict-pages`** is enabled.

### PDF Indexing and Tracking

Every generated document is cataloged via **`updatePDFManifest`** (lines 50-73), which appends metadata—including source HTML path, format specifications, and generation timestamps—to `data/pdf-index.tsv`. This manifest enables downstream TUI dashboard components to locate specific PDFs using tracker or report numbers.

## Command-Line Interface and Usage Examples

Generate a single CV with custom constraints:

```bash
node generate-pdf.mjs cv-output.html cv-output.pdf \
  --format=a4 \
  --report=018 \
  --max-pages=2 \
  --allow-reorder

```

Batch process multiple candidates with strict page limits:

```bash
node generate-pdf.mjs --batch=manifest.json \
  --format=letter \
  --max-pages=1 \
  --strict-pages

```

## Summary

- **Text sanitization** via `normalizeTextForATS` eliminates Unicode characters that break ATS parsers before rendering occurs.
- **Playwright Chromium** handles the HTML-to-PDF conversion, ensuring consistent cross-platform output that preserves semantic document structure.
- **Section validation** enforces canonical ordering by default, with optional reordering through `--allow-reorder` and [`config/profile.yml`](https://github.com/santifer/career-ops/blob/main/config/profile.yml) customization.
- **Page budgets** default to 2 pages, enforced by `enforcePageBudget` with optional strict-mode termination.
- **Manifest tracking** in `data/pdf-index.tsv` provides traceability between source HTML and generated PDF artifacts.

## Frequently Asked Questions

### What makes a PDF ATS-optimized according to the career-ops source code?

An ATS-optimized PDF in career-ops contains only ASCII-compatible characters after passing through `normalizeTextForATS`, maintains semantic HTML structure without complex nested tables, and follows predictable section ordering. The sanitization process specifically removes smart quotes, em-dashes, zero-width spaces, and unicode bullets that cause parsing failures in enterprise applicant tracking systems.

### How does career-ops handle special characters in CVs?

The system processes all content through `normalizeTextForATS` (lines 42-55 in `generate-pdf.mjs`) prior to Playwright rendering. This function implements regex-based substitution to replace typographic characters with their ASCII equivalents, ensuring that curly quotes become straight quotes and decorative dashes become standard hyphens before the PDF generation stage begins.

### Can I customize the section order in my generated PDF?

Yes. Define your preferred sequence in [`config/profile.yml`](https://github.com/santifer/career-ops/blob/main/config/profile.yml) under the `cv.sections` array, then invoke the generator with `--allow-reorder`. The `reorderCvSections` function will rearrange the HTML sections to match your specification while preserving the underlying markup structure, or you can run `/career-ops pdf` which automatically enables reordering.

### What happens if my CV exceeds the page limit?

By default, `enforcePageBudget` logs a warning when `countRenderedPdfPages` detects that the document exceeds the `--max-pages` threshold (default 2). If you specify `--strict-pages`, the pipeline throws a fatal error and terminates without updating the manifest, preventing oversized documents from entering the tracking system.