@thejob/resume-render
v1.11.0
Published
The resume renderer shared by thejob and the standalone resume builder: templates, pagination and HarfBuzz line breaking, identical in every browser and in the PDF.
Readme
@thejob/resume-render
The one resume renderer, shared by thejob (thejob-svelte-ui) and the
standalone resume builder (resume-builder-ui). resume-service's PDFs are each
app's print route running this in Chromium, so preview and PDF match.
<script>
import { ResumePaged, TemplateThumb } from '@thejob/resume-render';
</script>
<ResumePaged {resume} {labels} {levels} lang="en" imageUrl={(key) => url(key)} fit />| Export | What |
| --- | --- |
| ResumeDocument | a resume in its template, one continuous A4 sheet |
| ResumePaged | that sheet cut into A4 pages (fit to scale, print for the print route) |
| TemplateThumb | a template's live thumbnail with sample content |
| RESUME_TEMPLATES, templateSpec | the 100 template specs (traits, colour, font) |
| paginate, breakLines | the engines, for tests and tools |
@thejob/resume-render/pro (no Svelte, no DOM, so Node can import it):
proFeaturesUsed(layout, pages?) returns the billing feature keys a download
needs (Pro template, Pro design, multi-page). The editors warn with it and
resume-service's download gate enforces it, so the two cannot disagree. Free
templates are FreeResumeTemplates in @thejob/schema.
@thejob/resume-render/docx exports a resume or cover letter as Word.
imageUrl(key) turns a stored photo key into a URL: each app knows its own
image host.
Why it renders the same everywhere
linebreak.ts measures text with HarfBuzz (WebAssembly) over the TTFs in
src/lib/fonts and writes every line break itself, pins text-sized boxes, and
rounds line heights and spacing to whole pixels. resume-fonts.css and
linebreak.ts both load the fonts from /fonts/resume/, so HarfBuzz reads
exactly what the browser draws. Never swap the fonts for other copies.
Host app requirements:
- Vite, with
harfbuzzjsinoptimizeDeps.exclude(it locates its.wasmnext to its module). - This package's
dist/fonts/*.ttfcopied to the app'sstatic/fonts/resume/. Bundler-resolved font URLs render on screen, but Chromium dropped every glyph from the printed PDF.
Full design notes: thejob-svelte-ui/docs/RESUME-BUILDER.md.
Develop
pnpm build # svelte-package -> dist
pnpm check
pnpm yalc # build + yalc publish --push to linked appsyalc "pure" pushes do not refresh the copy pnpm resolves, so after a push copy
dist into each app's readlink -f node_modules/@thejob/resume-render, then
restart the app's dev server.
After any change to rendered output:
- Run the cross-browser gate:
thejob-resume-service/.qa/xbrowser.mjs(Chromium, Firefox, WebKit; where every page starts must match across all three, 120/120 for the QA resumes inthejob-test1). WebKit does not run on this Fedora host: start a Playwright server container listening onws://127.0.0.1:3333/first.RESandTPLenv vars narrow the run. - Render real PDFs through resume-service (
.qa/pdf.ts) in both apps and check they contain text (pdffontslists fonts,pdftotextprints the name) and the page count matches the preview. - Bump
PDF_VERSIONin resume-servicesrc/repositories/resumes.ts.
Layout of the package
| File | What |
| --- | --- |
| src/lib/resume-document.svelte | the template: one component, template traits as classes |
| src/lib/resume-templates.ts | the 100 template specs and their traits |
| src/lib/resume-paged.svelte | lays the document out off-screen, paginates, shows pages through clipped windows; sets data-paginated when done |
| src/lib/paginate.ts | where pages end (between blocks only, keep-with-next, widow/orphan rules) |
| src/lib/linebreak.ts | HarfBuzz line breaking and box pinning over the bundled fonts |
| src/lib/resume-format.ts | date and level formatting |
| src/lib/resume-fonts.css | @font-face for Resume Sans/Serif/Mono at /fonts/resume/ |
| src/lib/template-thumb.svelte | template gallery thumbnail |
| src/lib/pro.ts | proFeaturesUsed: which Pro features a design uses (the ./pro entry) |
| src/lib/fonts/ | the TTFs (shipped in dist/fonts) |
Labels and level names come in as props (labels, levels), so the package
holds no app copy or i18n.
