A preview you can trust
Real A4 sheets that paginate as you type, with a page count, “1 / 2” labels and zoom from 25% to 200%, or Fit.
A real PDF. No paywall at the end.
One clean template, a design panel to tune it, and a real PDF at the end — built after one too many CV sites that let you design the whole thing and then charge you to download it.
01Why it exists
A lot of CV sites work the same way. You pick a look, type in your whole working life, tune it until it’s right — and only then does the download button ask you to pay.
I didn’t want a subscription. I wanted a template, some boxes to fill in, and a PDF.
If I can make exactly what they’re selling, why pay for it? So I built my own, in about a day. It’s deliberately simple: one clean template, a panel to adjust it, and a real PDF at the end.
You do sign in — your CVs are saved to your account — but the download is just a download.
02Try it
This CV runs a page and a bit. Every block is measured at the printed width and dealt onto A4 sheets, the way the app does it — so change the density, the font or the spacing and watch the page count.
The quickest way to pull a CV back onto one page — adjusts size, leading, spacing and margins together.
Write more and watch it spill onto the next page. <b>, <i> and <u> work; any other tag is kept as text.
A named link prints the name; the PDF still links to the full address.
A working miniature of the builder: the renderer, the block measuring and the pagination are ported from the app, with three of its six metric-matched fonts loaded from Google Fonts. Simplified: pages sit side by side instead of stacked, nothing is saved anywhere (the status only mimics the 900 ms autosave), and Export opens your browser’s print dialog with the app’s print CSS, standing in for the server’s Chromium. Alex Morgan is invented.
03What it does
Real A4 sheets that paginate as you type, with a page count, “1 / 2” labels and zoom from 25% to 200%, or Fit.
900 ms after you stop, and again when the tab is hidden or closed. The status reads Saving, Unsaved or Saved — or Retry, if a save fails.
Sixty steps. Typing in one field merges into a single step while the keystrokes are under 700 ms apart. ⌘ Z, ⇧ ⌘ Z and ⌘ S work as you’d expect.
Three levels deep. Tab and Shift-Tab indent a bullet with everything under it; deleting one promotes its children instead of losing them. Paste an indented list and it splits into bullets.
One floating toolbar follows your selection. A link can have a name, so the CV prints “Portfolio” while pointing at the full address.
Detailed, Simple or Label-list sections, named as you like, and up to 50 CVs per account. On a phone it becomes Edit, Design and Preview tabs, with 16 px inputs so iOS doesn’t zoom.
04How it’s built
A Next.js app on Vercel. The editor keeps the whole CV in memory and saves it as plain database rows; the PDF is printed by a real browser on the server.
async function launchBrowser(): Promise<Browser> {
const puppeteer = (await import("puppeteer-core")).default;
if (isServerless) {
const chromium = (await import("@sparticuz/chromium")).default;
// No WebGL in a CV. Skipping it avoids unpacking swiftshader, which is a
// meaningful chunk of an already slow cold start.
chromium.setGraphicsMode = false;
return puppeteer.launch({
args: [...chromium.args, "--font-render-hinting=none"],
executablePath: await chromium.executablePath(),
headless: true,
});
}
The start of the export route, as it is in the repo. The browser is always closed in a finally block, so a failed render can’t leave Chromium running.
05Build notes
One pure-string renderer feeds both. It writes the CV as a flat list of blocks — the header, each section heading, each entry. The preview measures every block off-screen at the exact printed text width and deals them onto A4 sheets, collapsing margins the way CSS does and keeping a heading with its first entry. It measures again once the fonts arrive. In print, margins come from @page rather than padding, so page two gets a top margin too.
Vercel’s Chromium ships with Open Sans and nothing else, and a different font means different line breaks — so different page breaks. The app uses open fonts with the same metrics as Arial, Calibri, Georgia, Times and Cambria: Arimo, Carlito, Gelasio, Tinos and Caladea, with EB Garamond standing in for Garamond. The same files go to the browser and, inlined as base64, into the PDF’s HTML, because setContent() has no base URL to load them from.
puppeteer-core drives a compressed Chromium, with graphics off to shorten cold starts and the binary explicitly included in the function bundle. Export waits for a save to finish, then posts the CV to /api/export; the file comes back named after the person — Alex_Morgan.pdf.
Undo steps coalesce, saves queue so only one runs at a time, and anything typed during a save stays marked unsaved until the next one. A small sanitiser keeps bold, italic and underline and escapes every other tag, so List<String> survives instead of disappearing.