Export Pipeline
The dual-engine template model and the six client-side export formats.
Export Pipeline
Every VeriWorkly export — PDF, DOCX, HTML, Markdown, plain text, and JSON — is generated inside the user's browser. There is no server-side rendering step and no headless browser in the export path.
The one Playwright dependency is a test tool
Studio depends on Playwright for npm run test:parity, which lays the same document out in
Chromium and in @react-pdf/renderer and diffs the two box trees. It is a dev-only harness for
proving the preview and the export agree — it is not part of the export pipeline, and no
production code path launches a browser. There is no Puppeteer anywhere in the repository.
Dual-engine templates
Each resume template ships two implementations of the same document:
- Web engine (
web.tsx) — standard React and Tailwind CSS 4. Optimised for interactivity, live editing, and the paged on-screen preview. - PDF engine (
pdf.tsx) —@react-pdf/rendererprimitives (Document,Page,View,Text,Image) with a React-Native-style style object syntax, giving fixed-width layout control for print fidelity.
Cover letters follow the same split, with a shared CoverLetterPdf component that selects its layout
by templateId.
[!TIP] The live implementations are in the templates directory on GitHub.
The dispatcher
features/documents/export/export-dispatcher.tsx is type-driven. It switches first on the
document's type, then on the requested format, and uses an assertNever exhaustiveness guard on
both — so adding a document type or an export format is a compile-time error until every branch is
handled.
export async function exportDocumentByType(
document: BaseDocument,
format: ExportFormat,
): Promise<void> {
switch (document.type) {
case "RESUME":
return exportResumeDocumentByFormat(document, format);
case "COVER_LETTER":
return exportCoverLetterDocumentByFormat(document, format);
default:
return assertNever(document.type, "exportDocumentByType");
}
}Formats
| Format | Implementation | Notes |
|---|---|---|
@react-pdf/renderer | Vector text with embedded fonts. | |
| DOCX | the docx package | Editable Word output for further manual adjustment. |
| HTML | dedicated serialiser | Self-contained markup. |
| Markdown | dedicated serialiser | Plain, portable structure. |
| Plain text | dedicated serialiser | For paste-into-form ATS flows. |
| JSON | dedicated serialiser | The raw document model — re-importable into VeriWorkly. |
All six are available from a single export menu, both inside the editor and on a document's public share page.
Rendering pipeline
1. Normalisation
The active document is read from its Zustand store and normalised — dates formatted, rich-text fragments flattened, hidden sections dropped, section order applied.
2. Font registration
registerPdfFontById looks up the document's font in the shared FONT_REGISTRY and registers its
faces with @react-pdf/renderer, resolving font files to absolute URLs against the current origin.
Registration is memoised, so switching between documents does not re-register the same family.
A hyphenation callback breaks words longer than 18 characters into 12-character chunks, so a long unbroken URL or token cannot overflow the page.
3. Blob generation
The normalised data is passed to the template's pdf.tsx component, and pdf(...).toBlob() produces
the binary PDF in the browser.
4. Delivery
The Blob is converted to a temporary object URL and triggered as a download. The document's content
never leaves the device during export.
ATS considerations
- Vector text, not images — every glyph is real text, so applicant tracking systems can index the full contents.
- Embedded fonts — font files are embedded in the PDF, so layout does not shift based on the reader's locally installed fonts.
- Single-column-friendly layouts — the Precision ATS template in particular uses a dense single-column structure that parsers handle reliably.
- Document title metadata — the PDF's
titleproperty is set from the document owner's name.
What the PDF pipeline does not do
@react-pdf/renderer produces standard, untagged PDFs. VeriWorkly does not emit PDF/UA
accessibility structure tags, and does not populate author, subject, or keyword metadata — only
the document title. If you have a hard requirement for a tagged or fully-metadata-annotated PDF,
export to DOCX and generate the final file from a word processor.
Empty-field handling
formatDateRange and joinTruthy in features/documents/utils/formatters.ts drop blank parts
rather than substituting placeholder text, so an entry missing its dates, company, or location
exports with those pieces simply absent.
Titles are the one place a fallback still prints
An item with no title falls back to a generic label — "Role" for experience, "Education",
"Project", and "Item" for custom sections. These come from getExperienceRenderItems and its
siblings in templates/resume/shared/model.ts and are deliberate: a headingless entry would
otherwise render as a floating block of bullets.
Adding a template
The full procedure, including the skin.ts geometry contract and the contract tests that enforce
it, is documented once in
Resume Templates → Adding a template.
/pdf-debug/[type]/[templateId] is an internal PDF-rendering debugger useful while iterating. Its
navigation link is hidden in production builds, though the route itself has no server-side gate.