The resume builder that previews the actual PDF

Most builders show you an HTML approximation while you type, then render something different on export. We do not. Here is why that one decision changes the whole experience.

If you have used a resume builder in the last five years, you know the moment. You spend forty minutes filling out the form. The preview pane on the right looks clean. You hit "Download PDF". A different document comes out. The font is heavier. The spacing collapsed. A bullet got cut off at the bottom of page one. The accent color you picked looks slightly wrong on print. You re-open the builder, nudge the margins, hit download again, and the second PDF is wrong in a new way.

This happens because the preview is lying to you. It is not the PDF. It is an HTML approximation of what the PDF will probably look like, drawn by the browser, using fonts and metrics that do not match the export pipeline. The export is generated by a different renderer on a different machine, often in a different layout engine entirely.

The resume builder at /resume is built around one decision that fixes that problem. The preview pane is the PDF. The same renderer, the same fonts, the same layout, every time you press a key. What you see on the right is the file you are about to download. There is no render gap, because there is no second renderer.

This post is what that decision actually changes, and what we built around it.

The preview gap and where it comes from

Most resume builders are HTML-first. They are written as React or Vue apps where the resume is a tree of divs, the preview is just that tree styled with CSS, and the PDF is generated at the very end by a "print to PDF" pass or by sending the HTML to a server-side rendering service like Puppeteer or a paid PDF API.

That stack works fine when the preview matches the export. It rarely does. The reasons are mundane and stack on each other:

  • The browser uses one font hinting strategy. The PDF generator uses another. Letters shift by a fraction of a pixel, which becomes a fraction of a line, which becomes a missing bullet.
  • The browser handles page breaks with one set of rules. The PDF pipeline handles them with a different set. Content that lived comfortably on page one in the preview overflows in the export.
  • The browser does layout on a single viewport. The PDF lays out at print resolution with print-specific page margins. Anything that depended on viewport width is now wrong.

The polite name for what comes out is "approximation". The honest name is "drift".

You can paper over the drift with manual tuning. You can add a "preview in print mode" button that switches the CSS, or a separate "PDF preview" tab. You can run a render after every keystroke and show the user the result. Most of those workarounds make the experience worse, because they introduce a delay, a separate workflow, or a button you have to remember to press before you trust the preview.

We chose not to paper. We removed the second renderer.

What we render with, and why it matters

The builder uses react-pdf, which renders PDF documents directly from React components, in the browser, using the same layout engine that the export uses. The preview pane mounts a real PDF and lets you scroll, zoom, and inspect it the way you would in a PDF reader. The download button does not run a second pipeline. It hands you the same PDF you have been looking at.

The user-facing consequence is small in description and large in practice. You stop checking the export. You stop comparing the preview to the file. You stop re-opening the builder ten minutes after a download to fix a thing you did not notice in the HTML view. The trust gap is gone.

The implementation consequence is that everything we ship has to work in a PDF layout engine, not in a browser. That is more constraint than HTML, and it shapes the templates. Two-column layouts have to handle real column balancing. Headings have to handle real page breaks. Long bullet lists have to know what happens when they spill. We have leaned into that constraint, because the alternative was building something pretty in the browser and then watching it break on export.

What this unlocks for ATS

Applicant tracking systems do not read your resume the way a recruiter does. They open the PDF and look for a stream of text. They look for section headings they recognise. They look for dates in formats they parse cleanly. They look for hyperlinks they can follow. The fancier the layout, the more often they get confused.

Because our preview is the export, we can reason directly about what the parser will see. We are not guessing. We test the actual PDFs the builder produces against Greenhouse, Lever, Ashby, and Workday parsers, because those are the four that show up most often in the listings we curate.

A few things follow from that, baked into every template:

  • Real text, never images of words. Every heading, every bullet, every job title is selectable text in the PDF. Parsers can read it. Screen readers can read it.
  • Standard section names by default. Experience, Education, Skills, Projects, Awards. The parser expects them, the resume gives them. If you want to rename a section, you can, but the default is the safe one.
  • Single-column layouts available for the strictest parsers. The two-column templates pass most modern ATS cleanly, but if you are applying through a company that uses an older parser, the single-column option exists for a reason.
  • Hyperlinks in text, not buried in icons. The parser cannot follow an icon. It can follow a link with a visible URL or anchor text next to it.

None of this is keyword stuffing. We are not telling you to repeat the job description back to the recruiter or pad your resume with skills you do not have. We are saying: the parser cannot read what it cannot see, and we have made sure it can see what is on the page.

The four templates, in plain words

Four templates ship today. Each has a stance.

Classic is a clean single column with a calm blue accent. It reads well on every ATS and never fights the content. If you are unsure which template to pick, this is the safe choice.

Two Column uses a warm sidebar for skills, education, and contact, with experience on the right. It is dense without feeling cramped. Good for senior generalists with a lot to fit on one page.

Minimal uses a confident serif with teal accents. Built for senior roles that have a lot to say and want to say it without crowding. The whitespace is doing real work in this one.

Banner is a dense single column with green section markers. Engineering-leaning. If your resume is mostly bullets and you measure the page in lines per square inch, this is your template.

All four are free. The PDF export is free. There is no watermark on the file, on any template, paid or otherwise. Premium templates exist for users who want a different aesthetic, but the free set is genuinely complete on its own.

What we will not do at the download

A short list, because the resume builder category has earned its bad reputation honestly.

  • No email required to download. Open the builder. Fill it out. Hit download. The PDF is yours. We did not invent that workflow, but we are sticking to it.
  • No credit card on the free templates. Ever. Not a tiny "verify your card and we will not charge" upsell either.
  • No watermark on the file. Including on the free templates. The download is the document.
  • No data lock-in. Your draft lives in your browser until you choose to save it to an account. If you never sign in, you never have an account to delete.

Where to take this next

If you have read this far, the next move is to open the builder and write the first draft. It will take ten to fifteen minutes for a resume you can actually send. The PDF you download at the end is the same file you watched render on the right of the screen, which is how it should have worked all along.

The post after this one will get into how recruiters actually scan a resume in the first six seconds, and what that means for the order of sections on the page. If you want a heads-up when it lands, the RSS feed is here.