I ran my own CV through a text extractor and found three defects that would have degraded it in any automated screening pipeline. None of them were visible on screen.

Applicant tracking systems do not read your PDF the way you do. They run text extraction over it and work with whatever falls out. If you have never looked at that output, you do not actually know what your CV says. Here is what mine said.

Defect 1: letter-spaced headings

My section headings had a little CSS tracking on them, because it looks good:

h2 { text-transform: uppercase; letter-spacing: 1.1px; }

Extracted, EDUCATION came out as:

E D U C AT I O N

Tracking is implemented by positioning each glyph individually. An extractor sees gaps between glyphs and infers word boundaries. Parsers segment a CV by matching conventional headings — Experience, Education, Skills — and a heading that does not match means that section is not recognised as a section at all.

There is no way to keep the effect and the parse. Weight and colour carry hierarchy just as well.

Defect 2: a URL that wrapped

My LinkedIn URL sat in a narrow sidebar and wrapped across two lines. Extracted, it came out as:

https://linkedin.com/in/rubencipriano

The hyphen in ruben-cipriano was gone — the line break landed on it, and the extractor treated it as a soft hyphen and dropped it. The result is not a broken link, which you would notice. It is a plausible link to a profile that does not exist, which you would not.

The fix is unglamorous: white-space: nowrap on contact lines, and a font size chosen so the longest URL actually fits the column.

Defect 3: a two-column skills grid

Skills laid out as chips in a wrapping flex container looked tidy and extracted as nonsense, because the visual grouping is purely positional:

HTML5 CSS3 Front-End
React Next.js
Angular
Tailwind CSS
SCSS Bootstrap Back-End

Category labels interleave with values. A skills parser reading that finds Bootstrap Back-End and Redis Jenkins. The fix is structural: one label, then one uninterrupted comma-separated run. It still looks like a designed block; it just is not built out of independently positioned boxes.

The reading-order trap

There is a fourth issue specific to two-column CVs. Text extraction follows document order, not visual order. My sidebar was late in the DOM, so the extracted text began with my employment history and only reached my name and contact details near the end — after a stray RC monogram.

If you build a two-column layout with CSS grid, put the sidebar first in the markup and let the grid place it. Visual position and document order are independent, and only one of them is what machines read.

Generating instead of maintaining

All of this argues for generating the PDF rather than hand-maintaining it. I render HTML and print it with headless Chrome:

chrome --headless=new --disable-gpu \
  --no-pdf-header-footer \
  --print-to-pdf=out.pdf \
  file:///path/to/cv.html

Two things make Chrome the right tool here. It embeds a correct ToUnicode map for the fonts it subsets — which is what makes the text extractable at all, and what many PDF libraries get wrong, producing bullets and accented characters that decode as U+FFFD. And --no-pdf-header-footer suppresses the date and file:/// path Chrome otherwise stamps into the margins, which lands in the extracted text.

Assert it, do not remember it

The part that actually keeps this working is a verification step that runs on every build. It extracts the text from the PDF that is about to ship and asserts against it:

  • no U+FFFD replacement characters;
  • every contact URL present with its scheme, so link validators resolve it;
  • every conventional section heading present;
  • the name appearing before the first employer — a reading-order check;
  • each skills label followed by its own first value within a small character window;
  • a page budget, so a content edit cannot silently push a one-page CV onto a second page that ends up 30% full.

One warning from building this. My first run reported 24 replacement characters and I nearly went hunting for a font bug. The PDF was fine — the poppler build I was using defaults to Latin-1 output, and I was reading those bytes as UTF-8. Passing -enc UTF-8 to pdftotext fixed it. A verification tool that manufactures the defect it is checking for is worse than no tool, so pin the encoding explicitly.

Worth the afternoon

"ATS-friendly" is usually an adjective people apply to a template they have never tested. Turning it into assertions took an afternoon and found three real defects in a CV I had already revised several times. Run pdftotext over yours before you send it anywhere — the output is frequently a surprise.