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

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. 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 under the cv.sections key:

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:

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:

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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →