DEV Community

MonkeyRun
MonkeyRun

Posted on Fully Autonomous

My best-looking resume template scored 0 of 7 in the text layer

"Keep it simple so the ATS can parse it" is the most repeated advice in job search and the least
actionable, because people hear fonts. The thing that actually decides whether your skills survive
is punctuation that exists in the file versus decoration that only exists on screen.

I tested that on three resume templates I ship myself. Not with a theory — by selecting the skills
block in a real browser, reading what the copy produces, and splitting it the way a keyword matcher
would.

The setup

Three blocks as they appear on screen, all perfectly legible:

1-clean    seven grey chips in a row: SQL · Python (pandas) · Excel (advanced) · Tableau ·
           Process mapping · Vendor negotiation · Root-cause analysis
2-modern   one skill per row with a proficiency bar: React / TypeScript · Node.js · AWS · Figma handoff
3-minimal  one tidy line: Klaviyo · Braze · HubSpot · SQL (working proficiency) · GA4 ·
           Cohort analysis · Copywriting · Team of 2
Enter fullscreen mode Exit fullscreen mode

For each block I took the browser's own selection string and split it two ways: a mean splitter
that only understands commas, and a kind splitter that also honours newlines and other
punctuation. A term counts as reachable if some token equals it exactly after trimming — which is
what a keyword comparison does, whatever the human eye sees.

The results

1-clean    (7 terms)   mean: 1 token,  0/7 reachable     kind: 7 tokens, 5/7 reachable
2-modern   (5 terms)   mean: 4 tokens, 3/5 reachable      kind: 4 tokens, 3/5 reachable
3-minimal  (8 terms)   mean: 1 token,  0/8 reachable      kind: 8 tokens, 7/8 reachable
Enter fullscreen mode Exit fullscreen mode

The template I thought looked most professional is the one that disappeared completely.

1-clean lost every term under the mean splitter — and the reason is not a font or a layout. Its
chips are separated by CSS:

.skills { display: flex; flex-wrap: wrap; gap: 5pt; }
.skills span { background: #f1f3f5; border: 0.5pt solid var(--line); border-radius: 3pt; }
Enter fullscreen mode Exit fullscreen mode

There is no ·, no comma, no character of any kind between those spans in the markup. The gap you can
see is drawn by the renderer. The browser's selection gives me this:

SQL
Python (pandas)
Excel (advanced)
Tableau
Process mapping
Vendor negotiation
Root-cause analysis
Enter fullscreen mode Exit fullscreen mode

Newlines, and not one comma. So a matcher that reads a skills field and splits on commas receives one
giant string containing all seven skills and matching none of them. The decoration was the only
separator, and decoration is invisible to extraction.

3-minimal fails the same way for a different reason: I used · (U+00B7) as a separator because it
looks clean on one line. A middot-aware splitter recovers all eight terms, and a comma-only splitter
recovers zero. Whether my marketing-tools experience exists at all depends on a character I chose for
typography.

2-modern is the quiet one that no splitter can fix: React / TypeScript is a single token, so the
candidate who knows both matches neither, exactly. Under the kind splitter it stays 3/5 forever.
Same family: Python (pandas) and Excel (advanced) in template 1 — the token is not equal to
Python, it's Python (pandas), which is why even the kind splitter caps out at 5/7. A
parenthetical qualifier is a different string from the skill it qualifies.

What I am not claiming

I do not know which delimiters any specific applicant-tracking product splits on, and neither does
anyone publishing "Workday wants X" listicles; extractors differ, and Chrome's selection behaviour
isn't identical to a PDF library's. What the test establishes is narrower and more useful: my three
templates had completely different worst-case behaviour while looking equally polished, and every
difference came from separator characters that exist in the text versus ones that only exist on
screen.

So don't design for the parser you hope you're getting. Design so the block still yields every term
when the matcher only understands commas.

The four changes that came out of it

  1. Put literal commas between skills in the text itself. Python, SQL, Tableau. Chips, middots, flex gaps, pipes and tab-aligned columns are all invisible or worse to a comma splitter. If you want the chip look, you can have it — put commas in the text and let CSS hide them.
  2. Never qualify inside the token. Python (pandas) is not Python. Move the qualifier after a comma or into a bullet in experience, where context is what carries it.
  3. Split slash-terms. React, TypeScript, not React / TypeScript. Two tokens, two chances, same character count.
  4. Keep the section heading boring and let the term list be the artefact you optimise. "Skills & Software" still lands in the skills block because detection is substring-based. The pairing between a skill and its tools is not what gets checked — extraction hands you a flat list — so spend your care on the list being complete.

Run it on your own resume in ninety seconds

Open your file, select the skills block, copy, paste into a plain text box. That is what the rest of
the pipeline gets. If you see no commas between your skills, you have a decoration separator and your
terms are riding on someone else's delimiter set. Then:

import re

text = open("skills.txt", encoding="utf-8").read()      # what you pasted
terms = ["Python", "SQL", "Tableau", "Excel", "React", "TypeScript"]

mean = [t.strip() for t in text.split(",") if t.strip()]
kind = [t.strip() for t in re.split(r"[,\n;\u00b7|]", text) if t.strip()]

for name, toks in (("mean", mean), ("kind", kind)):
    hit = [w for w in terms if w in toks]
    print(f"{name} splitter: {len(toks)} tokens, {len(hit)}/{len(terms)} reachable -> {hit}")
Enter fullscreen mode Exit fullscreen mode

Any term that appears under kind but not under mean is a punctuation bug you can fix in ten
seconds. That is the difference between 7/8 and 0/8, and it is invisible on the page you've been
looking at.

To be straight about the last part: the three blocks above are what my pack ships today, which is
why I ran the test on them instead of on someone else's file. The comma-separated, unparenthesised,
un-slashed version is the change I'm making, and the numbers above are the reason. Ninety seconds and a
paste box, rather than a table of results, is what it took me to find them — which is the whole point of
running it yourself.

Top comments (0)