Curriculum Vitae as Code: Automation, CSS Print, and AI Resistance
A deep technical analysis on how to unify the web presence and the PDF document under a single source of truth, using Astro, Puppeteer, and a philosophy of "digital craftsmanship" that rejects automatic translation for precise surgery.
We live in an era where data redundancy is the silent enemy of productivity. As developers, we obsess over the DRY principle (Don’t Repeat Yourself), applying it to our functions and databases. However, paradoxically, we accept maintaining two versions of our professional identity: the Web Portfolio (interactive, modern, living) and the PDF Curriculum Vitae (static, rigid, bureaucratic).
This article dissects the technical solution I implemented to break that duality: a system where the web is the only source of truth (Source of Truth), and the PDF is simply an ephemeral projection, generated on demand, surgically sanitized, and delivered with millimeter precision.
The Convergence Problem
Maintaining a traditional CV in Word or Canva while developing a web portfolio creates an inevitable divergence. You update a project on the web, but forget the PDF. You correct a date in the PDF, but the web remains outdated.
The obvious solution is “print” the webpage. But anyone who has tried CMD + P on a modern web page knows that the result is disastrous: broken navigation buttons, strange margins, colors that don’t contrast, and page breaks that behead entire sections.
My objective was to create a flow where:
- Edit the code (.astro).
- I execute a command.
- I get a perfect A4 PDF, indistinguishable from one designed manually.
- The Architecture: Node.js as a Printing Press
The central piece is not a complex tool for generating PDFs like PDFKit or LaTeX, but rather the Chrome browser itself. Using Puppeteer, we can control a headless instance of Chrome to render the webpage.
The Master Script (generate-cv-pdf.js))
I’ve created a script that acts as a conductor. Its function is to lift a temporary server, generate documents, and then disappear.
// scripts/generate-cv-pdf.js (Simplified for clarity)
async function generatePDF() {
// 1. Iniciar Servidor Local (Blue-Green Deployment)
console.log('📦 Starting local server...');
const astro = spawn('npm', ['run', 'dev']);
// ... esperar a que localhost:4321 responda ...
// 2. Lanzar Navegador
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
// 3. Función generadora
const createPdf = async (urlPath, filename) => {
// Navegar a la versión Web
await page.goto(`http://localhost:4321${urlPath}`, {
waitUntil: 'networkidle0'
});
// Simular medio de impresión (CSS Print)
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'light' }]);
// Generar archivo físico
await page.pdf({
path: `public/${filename}`,
format: 'A4',
printBackground: true,
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' }
});
};
// 4. Ejecutar para ambos idiomas
await createPdf('/cv', 'Sandoval_Jose_2026.pdf');
await createPdf('/en/cv', 'Sandoval_Jose_2026_en.pdf');
// 5. Limpieza
await browser.close();
astro.kill();
}This script connects all the points:
- Edition: I edit
src/pages/cv.astro. - Server: The script launches that new version on port 4321.
- Render: Puppeteer visits the page as if it were a user.
- Styles: The browser applies the styles of
@media print(see section 3). - Output: The resulting PDF is saved in the public folder, ready to do a
git push.
Injection of Media (Puppeteer)
Here’s where the magic happens. It’s not just a screenshot; we’re telling Chrome to simulate being a printer before rendering.
// scripts/generate-cv-pdf.js
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'light' }]);
await page.pdf({
path: 'public/cv.pdf',
format: 'A4',
printBackground: true, // Importante para que se vean los badges de colores
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' }
});The beauty of this architecture is its simpllicity. There’s no intermediate conversion from HTML to PDF that loses styles. It’s the browser drawing pixels.
Metadata Injection (ATS Friendly)
It’s here where professional ethics come into play. Techniques like putting “white text on white background” to deceive bots are risky and penalized. The correct way to communicate content to an ATS is through standard PDF metadata.
Since Puppeteer doesn’t natively allow editing these fields, I integrated the pdf-lib library into the pipeline. Right after Chrome generates the visual file, we reopen it to inject the digital identity:
// Post-procesamiento después de generar el PDF
const injectMetadata = async (filename, meta) => {
const existingPdfBytes = fs.readFileSync(pdfPath);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Estos datos son leídos por los ATS antes que el contenido visual
pdfDoc.setTitle(meta.title); // "Curriculum Vitae - José Sandoval - 2026"
pdfDoc.setAuthor('José Sandoval');
pdfDoc.setSubject(meta.subject); // "Software Developer"
pdfDoc.setKeywords(meta.keywords); // ["Astro", "React", "Linux", ...]
const pdfBytes = await pdfDoc.save();
fs.writeFileSync(pdfPath, pdfBytes);
};This ensures that the file is indexed correctly by title, author, and keywords without the need for visual tricks.
Visual Sanitization: The Art of CSS Printing
The “Sanitization” is the process of cleaning up the user interface to turn it into a read-only document. The web is interactive; paper is passive.
The Subtraction Theory
Instead of creating a new stylesheet, we use @media print to subtract the unnecessary.
/* src/styles/global.css */
@media print {
/* Elementos de navegación web que no sirven en papel */
header, footer, nav, .no-print {
display: none !important;
}
/* Reseteo de colores para ahorrar tinta y mejorar contraste */
body {
background-color: white !important;
color: black !important;
}
}Semantic Relocation
An interesting challenge was the email. On the website it’s below; in the PDF it should be above. We used a “toggle” visual with CSS.
/* El email existe en el HTML pero está oculto por defecto */
.print-email-row {
display: none;
}
@media print {
/* En modo impresión, aparece mágicamente en el header */
.print-email-row {
display: flex !important;
}
}The Orphan Problem
The biggest enemy of automatically generated PDFs is the improper page breaks.
/* Prohibido cortar una experiencia laboral a la mitad */
article, .card, .experience-item {
break-inside: avoid;
page-break-inside: avoid;
}
/* Pegar títulos a su contenido */
h2, h3 {
break-after: avoid;
}The result is a reading flow that feels intentional, not accidental.
- The Decision Not to Use AI: The Cost of Precision
In this same project, I use language models (LLMs) like Llama 3 to automatically translate my technical articles. The temptation to connect the CV to this same flow was high. I imagined editing a JSON file in Spanish and seeing my CV magically appear in English.
However, as an analyst, I must perform Cost-Benefit Analysis and Risk Management.
The Risk of Technical Hallucination
A CV is a binary precision document.
- I’m saying “Pursuing a 3rd year of studies”, the translation cannot be “Graduated” or “Studying for 3 years”.
- If I talk about “React Hooks”, it can’t be translated as “Reaction Anchors”.
- If I mention “Seniority”, it cannot be interpreted as “Old Age”.
The LLMs, by nature, are probabilistic, not deterministic. They have a “temperature” that introduces creativity. In a blog post, creativity is welcome; an elegant synonym enriches the text. In a CV, creativity is a risk of distortion of reality.
Update Frequency
A blog updates weekly. A CV for a senior profile stabilizes and changes, perhaps, 3 or 4 times a year.
Implementing a robust “CV Data-Driven” architecture (separating data from view, validating translations, protecting HTML tags from translation) would have taken around 10-15 hours of engineering time. The time it takes me to manually translate a new paragraph in my CV is 5 minutes.
Mathematically, I would need 120 years of CV updates to amortize the development time of automation.
Digital Craftsmanship
Sometimes, the best tool isn’t the most complex one. I decided to maintain manual translation to ensure that each word in the English version has the exact connotation I want to convey to an international recruiter. It’s a compromise between quality and comfort.
Conclusion: Hybrid Flow
The final result is a hybrid system that automates the tedious and protects the critical:
- Edition: Manual and careful (Human).
- Translation: Precise and accurate (Human).
- Layout: Automatic and perfect (Machine).
- Generation: Snapshot and reproducible (Machine).
Now, my CV lives in code. Versioned in Git, backed by CI/CD, and ready to compile into a perfect PDF with a single command. We’ve achieved the immutability of digital presence.
6. Source Code & Demo
If you are interested in implementing this without all the context of my portfolio, I have extracted the core logic into a standalone repository. It contains the base HTML, the print CSS, and the Puppeteer + PDF-Lib script ready to use:
Automated CV Pipeline - Demo Repository
Automated translation (technical mode).