Skip to content
boussadjraPublic

About

Write a resume, see exactly what an applicant tracking system reads, and export a real PDF. Everything stays in your browser.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Repository files navigation

Rezuma

Write a resume, see exactly what an applicant tracking system will read, and export a real PDF. Everything stays in your browser.

pnpm install
pnpm dev

Why

Most resume builders show you a preview and hope. A preview proves nothing — it's pixels. The only question that matters is whether a parser can pull the text back out of the PDF, so Rezuma ships a test that does exactly that.

  • A real PDF, not a screenshot. Every glyph is drawn as vector text with the font embedded, so it stays selectable, searchable and extractable.
  • Proof, not promises. pnpm verify:pdf generates the PDF, extracts the text with pdf.js, and asserts every name, employer, date, bullet and skill survived the round trip — accents included.
  • Live ATS checks. Parseable dates, timeline gaps, quantified bullets, passive openers, keyword coverage. Each finding links to the field to fix.
  • English, French and Arabic, the last of them properly right-to-left — shaped and reordered in the PDF, not just mirrored on screen. See Arabic and right-to-left.
  • Four document layouts, all of which still export a linear text stream. See Layouts.
  • Nothing leaves your browser. See Privacy.

Privacy

Rezuma has no server and no account. It is a static site. Everything you type is held in your browser's localStorage under the key my-resume:v1, and it stays on your own machine.

  • No account, no email address, no sign-in.
  • No upload — the PDF is generated locally by your browser and downloaded directly. It is never transmitted anywhere.
  • No analytics, no trackers, no cookies, no third-party requests at runtime.
  • Closing the tab keeps your work. Clearing your browser data removes it, and nobody else ever had a copy to recover it from.
  • Save as JSON exports your document as a file you control; Open JSON reads one back. That is the only way data enters or leaves the app.

Because storage is per-browser and per-device, your resume will not follow you to another machine. That is the trade: no sync, no server, no copies.

Scripts

Command What it does
pnpm dev Dev server
pnpm build Type-check and build
pnpm preview Serve the production build
pnpm preview:cf Serve it the way Cloudflare will (wrangler)
pnpm deploy Build and deploy to Cloudflare
pnpm fonts Re-download the vendored TTFs into src/assets/fonts/
pnpm verify:pdf Generate PDFs and assert the text extracts back out

How the PDF is produced

src/services/pdf/ draws the document with jsPDF rather than rasterising the preview.

  • fonts.ts embeds Source Serif 4 or Source Sans 3 — or, for Arabic, Noto Naskh Arabic or Noto Sans Arabic — as subsetted TrueType, fetched on demand at export time so they stay out of the initial bundle. Two base-14 families (times, helvetica) are also selectable for maximum compatibility.
  • layout.ts is a two-pass typesetter: everything is measured into groups first, then paginated. That's what keeps a job title with its dates, a section heading with its first entry, and paragraphs free of orphans and widows. It paginates flows, so a layout with a side column is two of them sharing one page sequence.
  • exportPdf.ts turns a resume, a theme and a layout into those groups. It is written entirely in logical coordinates — offsets from the leading margin, which is the left one in English and the right one in Arabic — so one set of layout code serves both directions.

The preview loads the same families as webfonts, so what you see is what gets exported.

Why the fonts are vendored

jsPDF can only embed raw TrueType. @fontsource ships woff2, which it cannot read, so scripts/fetch-fonts.mjs pulls static TTFs from the Google Fonts CSS API (which serves TrueType when the request carries no User-Agent) into src/assets/fonts/. They are committed so builds are hermetic. All four families are SIL Open Font License; the license travels with them in that directory.

Arabic adds two requirements the script enforces. The CSS API answers a UA-less request with the latin slice unless you ask for subset=arabic — 24 KB with an empty Arabic cmap, which embeds without complaint and then draws nothing. And the face has to carry Arabic Presentation Forms-B (U+FE70–FEFF), because jsPDF shapes by substituting those codepoints rather than by running OpenType. pnpm fonts reads the cmap and refuses any face that fails either.

Arabic and right-to-left

Switching the interface to العربية sets dir="rtl" on <html>, and everything downstream follows from that one attribute: the stylesheets are written in logical properties, so the editor panel, the toolbar and the document all mirror without a direction-specific rule anywhere.

The PDF is the harder half, and it works:

  • Shaping. jsPDF substitutes Arabic Presentation Forms-B before drawing, so letters join. getTextWidth runs the same pass, which means measurement matches what is drawn.
  • Line breaking. splitTextToSize doesn't: it sums per-character widths of the unshaped string, and isolated forms are much wider than joined ones, so it ends lines ~20% short of the measure. Arabic gets a measured greedy wrap instead.
  • Extraction. The text comes back as presentation forms — ﻣﻬﻨﺪﺱ, not مهندس. String.normalize('NFKC') folds them back to the base letters, which is exactly what a parser's own normaliser does. pnpm verify:pdf asserts it.
  • What bidi does cost you. Reordering moves whole runs: a bullet mixing Arabic with React extracts with the Latin run displaced, and paired brackets come back mirrored. Every token survives, contiguity does not — so the verification checks Arabic token by token and says so.

Dates stay in Latin digits in Arabic (ar-u-nu-latn). Arabic-Indic digits are correct typography and unreadable to a date parser.

The interface is Arabic; the ATS check messages are not. They are English in all three locales — see Interface language.

Layouts

Four arrangements, orthogonal to the nine styles: any layout can wear any style. They live in src/config/layouts.ts, and both the preview and the exporter read that one file — the preview turns rail and gutter into a CSS grid, the exporter turns them into column geometry.

Layout What moves
Stacked Nothing. One column, headings above their content.
Side headings Section headings sit in a rail beside the body.
Timeline Each role's place and dates sit in a rail beside the entry.
Sidebar Skills, education and certifications move to a second column.

Only Sidebar prints two columns, and it is the one layout the ATS panel flags. The text stream stays linear — the main column is written before the side column, so a parser reads name → summary → experience → skills → education, and verify:pdf asserts that order — but an applicant tracking system that rebuilds lines from geometry rather than from the content stream will interleave the two anyway. The check says so rather than quietly passing.

Motion on the landing page

The landing page carries a set of Vue Bits-style effects — an animated aurora and light-ray field, a parallax dot grid, split-text and glitch headings, scramble-decrypt on the extracted-text panel, a typed terminal, counting figures, spotlight and tilt cards, magnetic buttons, a click-spark cursor, and an SVG diagram that draws itself.

None of them adds a dependency. They live in src/components/effects/ and are written against the platform: the aurora is one fragment shader on a fullscreen triangle in raw WebGL2, and the dot grid and sparks are canvas 2D. Reaching for ogl or three would have meant 150 kB+ of scene graph the page never uses, on the one route that is not lazily loaded — and Vue Bits ships under MIT + Commons Clause, which does not belong in an MIT repository. The aurora is the only piece heavy enough to matter (5 kB), and it is an async chunk loaded after mount.

Two rules hold for everything in that directory:

  • prefers-reduced-motion is the finished state, not a hidden one. Every effect renders its settled appearance and adds animation; nothing becomes unreachable when a reader turns motion off. The aurora paints one static frame, the dot grid stops tracking the pointer, and every reveal starts revealed.
  • Nothing changes what is announced. Split, scrambled and counted text each keep an intact copy in the accessibility tree with the animated copy marked aria-hidden. The one exception is the extracted-text panel, which renders a single node on purpose — selecting and copying that block is the demonstration, and a hidden duplicate would put every line in the clipboard twice.

Loops are tied to an IntersectionObserver and to visibilitychange, so a background tab or a scrolled-past section costs nothing.

Verifying it is machine-readable

AR / classic / sidebar
  file        ar-classic-sidebar.pdf (65 KB, 1 page)
  extracted   2563 chars
  checks      51/51 found
  order       summary → experience → skills → education
  arabic      1554 letters (61% of the text)

Nine cases: three languages, two styles, four layouts. pnpm verify:pdf exits non-zero on any miss, so a regression in the exporter fails loudly rather than silently shipping an unreadable file. The expected section order is derived from layouts.ts rather than hard-coded, which keeps it a test of the exporter instead of a copy of it.

Deploying

Static output — no server, no runtime, no environment variables.

Cloudflare Workers

The repo is set up for Workers static assets, which is what the dashboard creates today for a Vite project. Connect the repo under Workers & Pages → Create → Workers → Import a repository and use:

Setting Value
Build command pnpm build
Deploy command npx wrangler deploy
Output directory dist

wrangler.jsonc supplies the rest. It declares a static-asset-only Worker — no main, so no server-side code runs — and sets not_found_handling to single-page-application, which is what stops a hard refresh on /app from 404ing.

Files that matter to a deployment:

  • wrangler.jsonc — the Worker definition. It must be committed, otherwise wrangler deploy drops into an interactive setup wizard, which fails in CI.
  • public/_headers — immutable caching for hashed assets and fonts, plus X-Content-Type-Options, Referrer-Policy, X-Frame-Options and a Permissions-Policy that switches off APIs this app never uses. Honoured by Workers static assets, Pages and Netlify alike.
  • .node-version / packageManager — pin Node 22 and pnpm 11. The lockfile is version 9.0, which a build image defaulting to pnpm 8 cannot read.
  • pnpm-workspace.yaml — allowBuilds must list esbuild and workerd. pnpm 11 treats an ignored install script as a hard error, and wrangler depends on both, so omitting them fails the deploy step.

There is deliberately no public/_redirects. The usual SPA rule (/* /index.html 200) is rejected by the Workers deploy API with Infinite loop detected in this rule, because static assets already strip /index and .html from paths, so redirecting to /index.html would re-enter the same rule. not_found_handling is the supported mechanism here. Note that wrangler dev accepts the file even though the deploy API refuses it, so this only surfaces on a real deploy.

Check the built output the way Cloudflare will actually serve it:

pnpm build
pnpm preview:cf      # wrangler dev, using wrangler.jsonc

Or deploy by hand from your machine with pnpm deploy.

Other hosts

vercel.json is kept for Vercel (same rewrite and headers, different syntax).

For Cloudflare Pages or Netlify, add a public/_redirects containing:

/*    /index.html   200

Those platforms want the rule that Workers rejects. Any static host works provided unmatched routes fall back to index.html.

Source layout

Source lives under src/, grouped by the role a file plays rather than by feature:

src/
  components/   appearance/ · ats/ · editor/ · effects/ · preview/
  composables/  useAppearance · useResume · useTheme · useReducedMotion · useRaf
  config/       themes.ts — the nine styles · layouts.ts — the four layouts
  data/         resume-{en,fr,ar}.json (the samples) · landing-demo.ts
  i18n/         index.ts + locales/{en,fr,ar}.ts
  router/
  services/     ats.ts (the checks) · pdf/ (the exporter)
  styles/
  types/
  utils/        format.ts · id.ts · normalise.ts
  views/

@/ resolves to src/, so imports read the same wherever they sit in the tree.

Data

src/data/ holds one fictional sample document per language. English and French share a person (Alex Moreau, who does not exist); the Arabic sample is Mohammed Al-Qasimi, who also does not exist, with the same career relocated to Saudi Arabia — a market where the document would actually be read in Arabic. Reset to sample restores them and is undoable.

Interface language

English, French and Arabic, via vue-i18n. Messages live in src/i18n/locales/; en.ts is the reference and the other two are typed against it, so a key added without a translation is a type error rather than a silent fallback. The active locale is the single source of truth for language: setLocale writes lang and dir onto <html>, the resume store reads the locale to pick which of the three documents is on screen, and the date formatters read it to pick an Intl locale.

Each language keeps its own document and its own undo history, all three in localStorage under the same key. Switching language switches documents; it does not translate anything.

The landing page is translated too, and carries its own language picker so a visitor who arrives in Arabic never has to open the editor to find one. Two things on it stay left-to-right by design: the verify:pdf transcript, because terminal output is a left-to-right artefact in every locale, and the Latin type specimen, because it is showing a Latin face on purpose. The privacy diagram does mirror — it is a sentence about where your data goes, not a fixed picture.

The ATS check messages are English in all three locales. The detection is trilingual (Arabic units and Arabic-Indic digits count as quantified results; تم and جرى are flagged as agentless passives the way "responsible for" is), but the prose explaining each finding is not yet translated.

Stack

Vue 3, TypeScript, Vite, vue-router, vue-i18n, jsPDF, pdf.js. No backend.

License

MIT — see LICENSE.

About

Write a resume, see exactly what an applicant tracking system reads, and export a real PDF. Everything stays in your browser.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages