name: CV PDF/HTML Render Playbook description: Standaard werkwijze voor stabiele A4 CV templates met web/PDF-pariteit, inclusief paginering, meting en debug-stappen.
CV PDF/HTML Render Playbook (Cevace / YorFutur)
Deze skill is de praktische leidraad voor het bouwen en tunen van CV templates zodat:
- de browser preview en PDF hetzelfde gedrag tonen;
- A4-marges stabiel blijven;
- page-breaks voorspelbaar zijn;
- we sneller kunnen debuggen zonder opnieuw het wiel uit te vinden.
1. Kernprincipe: Altijd 3 lagen synchroon
Elke template met paginering heeft minimaal 3 lagen die op elkaar moeten aansluiten:
- Render (HTML preview)
Voorbeeld:components/cv-templates/*PagedTemplate.tsx - Measure (hoogtemeting off-screen)
components/cv-templates/CVContentMeasure.tsx - Pagination logic (blokverdeling over pagina’s)
hooks/useCVPagination.ts
Als je maar 1 laag aanpast, krijg je bijna altijd mismatch tussen browser en PDF of onverwachte breaks.
2. Bestandskaart per template
Voor nieuwe of bestaande templates altijd deze map volgen:
- Web template:
components/cv-templates/<Template>PagedTemplate.tsx - PDF template:
components/cv-templates/<Template>PagedTemplatePDF.ts - Meting:
components/cv-templates/CVContentMeasure.tsx - Paginering:
hooks/useCVPagination.ts - Registratie template-id/labels:
types/cv-templates.ts+ eventuele registry-bestanden
3. A4-regels (praktisch)
- A4 =
210mm x 297mm - Kleine mm-aanpassingen lijken soms “sprongsgewijs” effect te geven door:
- block-splitting (atomic chunks),
- orphan-protection,
- line wrapping op woordgrenzen.
Richtlijn:
- ~2 regels extra ruimte is vaak circa 8-10mm effectief, maar altijd in combinatie met paginering.
4. Standaard wijzigingsvolgorde
Bij requests als “nog 2 regels onderaan” of “talen toevoegen”:
- Template render aanpassen (TSX/PDF)
Layout, volgorde, labels, secties. - Measure aanpassen
Exact dezelfde blokken/volgorde meetbaar maken metdata-block-id. - Pagination aanpassen
getTemplateUsableHeightPx+ eventuele template-specifieke reserve/orphan-rules. - Verifiëren
npm exec tsc --noEmit+ visuele check browser + PDF.
5. Bekende valkuilen en oplossing
A) Bottom overflow op pagina 1
Symptoom:
- Tekst loopt onderaan tegen rand of breekt te vroeg.
Oplossing:
- Niet alleen padding wijzigen.
Altijd zowel:- bottom padding in
*PagedTemplate.tsx - usable height in
useCVPagination.ts
- bottom padding in
B) Sectie aanwezig in PDF maar niet in web (of andersom)
Symptoom:
- Bijvoorbeeld
Talenontbreekt in preview of schuift vreemd.
Oplossing:
- Sectie toevoegen in alle 3 lagen:
- render,
- measurement (
data-block-id), - pagination block creation (
createBlocks).
C) Grote sprongen bij mini-aanpassingen
Symptoom:
- 0.5-1mm wijziging geeft ineens meerdere regels verschil.
Oplossing:
- Check of blokken atomic gesplitst worden (
splitHtmlContent). - Check orphan protection voor dat template.
- Tune in kleine stappen, maar op bloklogica (header+first body fit), niet alleen op mm.
6. Professionele template-aanpak (specifieke notitie)
Als we professional aanpassen:
Talendirect bovenOpleidingenplaatsen in render én blockvolgorde.- Bottom fix op pagina 1 uitvoeren via bewezen dubbel-aanpak:
- template padding
- template usable-height in paginator
- Daarna pas micro-finetune.
7. Definition of Done
Template is “klaar” als:
- Browser preview en PDF dezelfde sectievolgorde tonen.
- Geen tekst tegen top/bottom rand op pagina 1 of 2+.
- Geen extra lege eindpagina.
- Belangrijke secties (
Profiel,Kernvaardigheden,Talen,Werkervaring,Opleidingen) correct aanwezig volgens afgesproken volgorde. - TypeScript check is groen.
8. Snelle debug-checklist (copy/paste)
- Is de sectie toegevoegd in render?
- Is de sectie toegevoegd in
CVContentMeasuremetdata-block-id? - Is de sectie opgenomen in
createBlocksinuseCVPagination? - Kloppen template-specifieke heights in
getTemplateUsableHeightPx? - Is orphan protection te streng voor dit template?
- Is bottom padding consistent met verwachte usable space?
npm exec tsc --noEmitgroen?
9. Server-Side PDF Generatie in Moderne Next.js App Router Routes
Het Probleem: React Error #31 & RSC JSX Runtime Conflict
In moderne Next.js App Router routes kunnen JSX-tags in Server Components, Server Actions en Route Handlers door de React Server Components (RSC) JSX-runtime van Next.js worden gebundeld.
Wanneer deze getranspileerde objecten rechtstreeks worden doorgegeven aan @react-pdf/renderer (bijvoorbeeld via renderToBuffer of pdf().toBlob()), faalt de custom reconciler met:
Minified React error #31: Objects are not valid as a React child (found: object with keys {$$typeof, type, key, ref, props, _owner, _store}).
Dit komt doordat de Next.js bundler JSX compileert tot RSC-metadata in plaats van de standaard client-side elementen die @react-pdf/renderer verwacht.
De Oplossing: Geïsoleerde Node-processen
Om de Next.js Webpack/Turbopack bundler en RSC-runtime volledig te omzeilen:
- Plaats PDF-render logica in een extern script:
Maak een los TypeScript script aan (bijvoorbeeld in
scripts/generate-pdf.tsofscripts/generate-cv-pdf.ts). - Importeer en compileer direct in Node:
Laat het script data inlezen via een JSON-bestand of stdin, roep
React.createElementenrenderToBufferofrenderToFileaan, en schrijf het resultaat weg. Vermijd JSX in het PDF-document zelf als het document ook door Next kan worden gebundeld; gebruik danReact.createElementin de PDF-component. - Voer uit met
npx tsx: Gebruikchild_process.exec(ofexecAsync) vanuit de Route Handler of Server Action om het script geïsoleerd te draaien:await execAsync(`npx tsx scripts/generate-pdf.ts "${inputJsonPath}" "${outputPdfPath}"`); - Schakel logs uit:
Demp
console.login het externe script om te voorkomen dat stderr of stdout vervuild raakt en buffers beschadigd raken. - Valideer het resultaat:
Controleer in de route dat de output begint met
%PDF, zetContent-Type: application/pdf,Content-Disposition,Cache-Control: private, no-store, max-age=0en ruim scratch-bestanden altijd op infinally.
Cevace-voorbeelden
- CV PDF:
app/api/generate-cv-pdf/route.ts+scripts/generate-cv-pdf.ts - Pro rapport:
app/api/pro-reports/[reportId]/route.ts+scripts/generate-pdf.ts - Assessment rapport:
app/api/assessment/report/route.ts+scripts/generate-assessment-report-pdf.ts
Assessmentrapport-specifieke notitie
- Template:
components/pdf/AssessmentReportPDF.tsx - Logo:
public/logo/Cevace-zwart-stapel.jpg, in het externe script ingelezen als data-URL zodat de serverroute niet afhankelijk is van externe assets. - Brandingregel: Cevace-logo zichtbaar in de header; titel en assessmentmetadata ruim scheiden met margin/padding en een subtiele lijn.
- Regressietest:
tests/e2e/assessment-v2.spec.tsbevat een PDF-downloadtest diecontent-type, bestandsnaam,%PDFen bestandsgrootte controleert.
Voorbeeld Script (scripts/generate-pdf.ts):
import React from 'react';
import { renderToBuffer } from '@react-pdf/renderer';
import MyDocumentPDF from '../components/pdf/MyDocumentPDF';
import fs from 'fs';
async function main() {
const [inputPath, outputPath] = process.argv.slice(2);
try {
const data = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
// Voorkom stdout vervuiling
console.log = () => {};
console.warn = () => {};
console.info = () => {};
const element = React.createElement(MyDocumentPDF, { data });
const buffer = await renderToBuffer(element);
fs.writeFileSync(outputPath, buffer);
process.exit(0);
} catch (err: any) {
process.stderr.write(`Error: ${err.message}\n`);
process.exit(1);
}
}
main();