Resume Renderer
A résumé renderer where templates are CSS only, so they can’t fabricate anything, and every theme is measured against real text extraction.
- Role
- Solo
- Released
- v0.1.0, Sep 2026
- Stack
- TypeScript; Python, WeasyPrint and poppler for verification
- License
- MIT
What it is
A TypeScript library that turns a structured résumé into HTML, styled by a theme. It started as the module that renders Artemis’s tailored résumés into something a person can submit. It’s published on its own because the problem isn’t specific to Artemis: most résumé templates are designed and then hoped about.
résumé data (JSON) ──render()──▶ one canonical HTML ──+ theme.css──▶ PDF
(browser print, WeasyPrint, Playwright)Artemis is its first consumer. Tailored résumés render through it in the web app, which downloads PDF bytes rendered on the server; two extension paths still use an older text parse.
Templates are CSS only
render() emits one semantic HTML structure for every résumé, and a template is a single stylesheet. A template can restyle text and hide it, but it cannot add, remove or reorder it. Three things follow from that.
- A template can’t invent anything. Everything in the output was in the input, which matters when the input comes from a language model.
- A template written by someone else is safe to accept. Sandboxing CSS is a solved problem; accepting someone’s HTML and JavaScript is not.
- Extraction is solved once for the structure, and each theme only has to be checked for what its styling does to it.
The cost is that every layout difference has to be expressible in CSS. Where a design needed something CSS couldn’t do, the answer was a hook in the canonical HTML, never markup in the template. Each theme’s manifest carries the rest: section order, label overrides, and a few layout flags, such as setting the summary inside the header, or leading with the institution instead of the role for an academic CV.
Measuring extraction
A résumé that looks right can still reach an applicant tracking system as garbage. The failures are specific and repeatable: letter-spacing that splits a word into single characters, an email broken across two lines, a right-aligned date landing inside a wrapped job title, a margin label spliced into a paragraph.
Every theme goes through two checks, with its own manifest defaults applied, so what gets verified is exactly what ships.
- A markup-aware check renders the theme against four test résumés, from a near-empty first-year one to a pathological one with missing fields and 60-character email addresses. It extracts the text in two pdftotext modes and verifies that every string survives, in order, with section labels and contact details intact.
- A structure-blind check doesn’t know the markup. It grades what a generic parser working from a heading lexicon would recover, across three different extractors, on a base résumé and four variations: a short name, no research section, one job with no summary, and flat skills.
| Badge in Artemis | Themes |
|---|---|
| ATS-safe: safe to submit through applicant-tracking portals | classic, folio, marginalia, minimal, numerals, standfirst |
| In person: for a reader, not a parser — email it or hand it over | academic, broadsheet, colorfield, ledger, masthead |
| In person only: submit by hand; its two-column flow interleaves when extracted | spread |
| No badge: the plain structural theme for unattended generation | plain |
ClassicATS-safe
FolioATS-safe
MarginaliaATS-safe
MinimalATS-safe
NumeralsATS-safe
StandfirstATS-safe
AcademicIn person
BroadsheetIn person
ColorfieldIn person
LedgerIn person
MastheadIn person
SpreadIn person only
PlainServer-sideEvery theme keeps all of its text in every extraction mode. Reading order holds in every mode for the single-column themes. The two two-column themes are labelled expressive, and each manifest records exactly which modes keep their order.
- A linter with ten rule families, each written after a failure it now catches: no letter-spacing on body text, no grids that rely on source order, no display: contents, which WeasyPrint ignores, and so on. A regression stylesheet reproduces every past defect, and the linter has to flag all of them.
- A pixel-level visual regression against reference renders, for layout breaks that text extraction can’t see.
Fitting to a page
Every bullet carries a priority and every theme exposes a density variable. Fitting a résumé to a page count is a search over those two: tighten density first, then drop the lowest-priority bullets until the page target holds. No text is rewritten, so fitting never changes what the résumé says. The search also catches overflow inside multi-column layouts, which doesn’t add a page and so wouldn’t show up in a page count.
Each manifest records how many bullets fit on one page at a fixed 10-point body, so a caller can ask how much room a theme has before fitting anything.
The contract
The input is a versioned JSON schema, mirrored as a Pydantic model for Python backends. That model is also where loose input from a language model gets cleaned up before it reaches the renderer. Three details matter more than they look.
- Dates are kept as written. The original string is always present and is what gets rendered; parsed start and end dates sit alongside it for sorting. A date nobody can parse still prints correctly.
- Bullets carry their provenance. Each can point back to the entry in the user’s pool it came from, as a hard reference rather than a relevance score.
- Nothing disappears silently. When a section is omitted, an entry loses all its bullets, or a field can’t be rendered, the renderer returns a warning with the path to what was dropped.
render() returns HTML and nothing else. PDF is the caller’s choice: the browser’s print dialog, WeasyPrint or Playwright. That keeps the library free of dependencies and identical in Node and the browser.
How the themes were made
Each of the twelve designed themes was transcribed from a Claude Design original and compared with it side by side. The transcription tool matches the design’s text rather than its markup, so it can recover the styling of each element, the name, a section label, a date, even when the original’s structure looks nothing like the canonical one. A thirteenth theme, plain, is meant for unattended server-side rendering.
Every font is under the OFL. Two are condensed instances cut from Archivo’s width axis, because WeasyPrint ignores font-stretch.
Limits
- Publications and certifications are in the schema but not rendered yet. Passing them raises a warning instead of dropping them.
- Not on npm. It installs from the repository.
- No normalize() in TypeScript. Loose input is cleaned by the Python model.
- The plain theme isn’t verified in its target engine yet. A probe from Artemis found that engine doesn’t honor ligatures. Missing hyphens in extracted text come from poppler’s de-hyphenation and affect every theme; they aren’t a rendering defect.
- The extraction checks test the text layer, not the behaviour of any particular vendor’s parser.
- Two-column themes lose reading order when a parser reads by position on the page.