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