If you've ever run your own resume through an ATS (Applicant Tracking System) parser and watched
your job title or bullet points come out garbled, you've hit a problem that has nothing to do with
your content and everything to do with how the PDF was built.
The root cause: PDF is a layout format, not a text format
A PDF doesn't store "a paragraph followed by another paragraph." It stores absolute-positioned
glyphs on a page. Most polished-looking resume templates get that layout from HTML+CSS rendered
through a headless browser (Puppeteer/Chromium), which is great for visual fidelity and terrible
for text order: multi-column layouts, sidebars, and floated elements often get flattened in
reading order that has nothing to do with visual order. An ATS parser reads the underlying text
stream top-to-bottom, left-to-right in insertion order — so a two-column template can silently
interleave your work history with your skills list, or drop your most recent job title into the
middle of a bullet point.
What actually fixes it
Two things matter more than template beauty:
-
Build the PDF with a low-level drawing API, not a browser. Libraries like
pdfkitlet you place text in the exact order you write it in code — single-column templates render sidebar accents as decoration (colored rectangles, icons) that never enter the text stream, so the parser only ever sees the real content, in the real order. - Always keep a resume-only fallback path. If you're generating resume content with an LLM (turning raw notes into structured bullet points, for example), the model call will eventually fail or time out. A parser that falls back to a deterministic, rule-based structuring of the same raw input — instead of failing the whole request — means the user still gets a correct, ATS-safe PDF even when the AI provider has a bad day.
A concrete example
I ended up building this into CV Forge, a small AI resume generator: raw notes
go in, an LLM (OpenAI-compatible, tested against Groq's Llama 3.3 70B) turns them into a structured
CV JSON, and pdfkit renders it with a single-column-first "Compact" template designed to stay
ATS-safe, alongside two more visual templates for when you're sending directly to a human. If the
AI call fails, a local parser produces the same structured JSON so the PDF still comes out
correctly formatted.
The lesson generalizes past resumes: any time you're generating a PDF that a machine will read
back, decide before picking a rendering approach whether text-stream order matters as much as
visual layout. If it does, skip the headless browser and reach for a library that lets you control
the text stream directly.
Top comments (0)