CV como Código: Automatización, CSS Print y la Resistencia a la IA
Un análisis técnico profundo sobre cómo unificar la presencia web y el documento PDF bajo una sola fuente de verdad, utilizando Astro, Puppeteer y una filosofía de 'artesanía digital' que rechaza la traducción automática por precisión quirúrgica.
Vivimos en una era donde la redundancia de datos es el enemigo silencioso de la productividad. Como desarrolladores, nos obsesiona el principio DRY (Don’t Repeat Yourself), aplicándolo a nuestras funciones y bases de datos. Sin embargo, paradójicamente, aceptamos mantener dos versiones de nuestra identidad profesional: el Portafolio Web (interactivo, moderno, vivo) y el Curriculum Vitae en PDF (estático, rígido, burocrático).
Este artículo disecciona la solución técnica que implementé para romper esa dualidad: un sistema donde la web es la única fuente de verdad (Source of Truth), y el PDF es simplemente una proyección efímera, generada bajo demanda, sanitizada quirúrgicamente y entregada con precisión milimétrica.
1. El Problema de la Divergencia
Mantener un CV tradicional en Word o Canva mientras desarrollas un portafolio web crea una divergencia inevitable. Actualizas un proyecto en la web, pero olvidas el PDF. Corriges una fecha en el PDF, pero la web sigue desactualizada.
La solución obvia es “imprimir” la página web. Pero cualquiera que haya intentado CMD + P en una web moderna sabe que el resultado es desastroso: botones de navegación rotos, márgenes extraños, colores que no contrastan y cortes de página que decapitan secciones enteras.
Mi objetivo fue crear un flujo donde:
- Edito el código (
.astro). - Ejecuto un comando.
- Obtengo un PDF A4 perfecto, indistinguible de uno diseñado manualmente.
2. La Arquitectura: Node.js como Imprenta
La pieza central no es una herramienta compleja de generación de PDFs como PDFKit o LaTeX, sino el propio navegador Chrome. Utilizando Puppeteer, podemos controlar una instancia de Chrome “sin cabeza” (headless) para renderizar la web.
El Script Maestro (generate-cv-pdf.js)
He creado un script que actúa como director de orquesta. Su función es levantar un servidor efímero, generar los documentos y luego desaparecer.
// 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();
}Este script conecta todos los puntos:
- Edición: Yo edito
src/pages/cv.astro. - Servidor: El script levanta esa nueva versión en el puerto 4321.
- Render: Puppeteer visita la página como si fuera un usuario.
- Estilos: El navegador aplica los estilos de
@media print(ver sección 3). - Output: El PDF resultante se guarda en la carpeta pública, listo para hacer
git push.
La Inyección de Medios (Puppeteer)
Aquí es donde ocurre la magia. No es una simple captura de pantalla; le indicamos a Chrome que simule ser una impresora antes de renderizar.
// 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' }
});La belleza de esta arquitectura es su simplicidad. No hay conversión de HTML a PDF intermedia que pierda estilos. Es el navegador dibujando píxeles.
La Inyección de Metadatos (ATS Friendly)
Aquí es donde entra la ética profesional. Técnicas como poner “texto blanco sobre fondo blanco” para engañar a los bots son riesgosas y penalizadas. La forma correcta de comunicar el contenido a un ATS es a través de metadatos estándar del PDF.
Como Puppeteer no permite editar estos campos nativamente, integré la librería pdf-lib en el pipeline. Justo después de que Chrome genera el archivo visual, lo reabrimos para inyectar la identidad digital:
// 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);
};Esto asegura que el archivo sea indexado correctamente por título, autor y palabras clave, sin necesidad de trucos visuales.
3. Sanitización Visual: El Arte del CSS Print
La “Sanitización” es el proceso de limpiar la interfaz de usuario para convertirla en un documento de lectura. La web es interactiva; el papel es pasivo.
La Teoría de la Sustracción
En lugar de crear una hoja de estilos nueva, utilizamos @media print para sustraer lo innecesario.
/* 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;
}
}La Reubicación Semántica
Un desafío interesante fue el correo electrónico. En la web está abajo; en el PDF debe estar arriba. Usamos un “toggle” visual con 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;
}
}El Problema de los Huérfanos
El mayor enemigo de los PDFs generados automáticamente son los saltos de página inadecuados.
/* 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;
}El resultado es un flujo de lectura que se siente intencional, no accidental.
4. La Decisión de NO Usar IA: El Costo de la Precisión
En este mismo proyecto, utilizo modelos de lenguaje (LLMs) como Llama 3 para traducir automáticamente mis artículos técnicos. La tentación de conectar el CV a este mismo flujo fue alta. Imaginaba editar un JSON en español y ver aparecer mágicamente mi CV en inglés.
Sin embargo, como analista, debo realizar análisis de Costo-Beneficio y Gestión de Riesgos.
El Riesgo de la Alucinación Técnica
Un CV es un documento de precisión binaria.
- Si digo “Cursando 3° año”, la traducción no puede ser “Graduado” ni “Estudiando 3 años”.
- Si hablo de “React Hooks”, no puede traducirse como “Ganchos de Reacción”.
- Si menciono “Seniority”, no puede ser interpretado como “Vejez”.
Los LLMs, por naturaleza, son probabilísticos, no deterministas. Tienen una “temperatura” que introduce creatividad. En una entrada de blog, la creatividad es bienvenida; un sinónimo elegante enriquece el texto. En un CV, la creatividad es un riesgo de distorsión de la realidad.
La Frecuencia de Actualización
Un blog se actualiza semanalmente. Un CV de un perfil senior se estabiliza y cambia, quizás, 3 o 4 veces al año.
Implementar una arquitectura robusta de “CV Data-Driven” (separando datos de vista, validando traducciones, protegiendo tags HTML de la traducción) habría tomado unas 10-15 horas de ingeniería. El tiempo que me toma traducir manualmente un párrafo nuevo en mi CV es de 5 minutos.
Matemáticamente, necesitaría 120 años de actualizaciones de CV para amortizar el tiempo de desarrollo de la automatización.
La Artesanía Digital
A veces, la mejor herramienta no es la más compleja. Decidí mantener la traducción manual para garantizar que cada palabra en la versión inglesa tenga la connotación exacta que quiero transmitir a un reclutador internacional. Es un compromiso con la calidad sobre la comodidad.
5. Conclusión: Un Flujo Híbrido
El resultado final es un sistema híbrido que automatiza lo tedioso y protege lo crítico:
- Edición: Manual y cuidadosa (Humano).
- Traducción: Manual y precisa (Humano).
- Maquetación: Automática y perfecta (Máquina).
- Generación: Instantánea y reproducible (Máquina).
Ahora, mi CV vive en el código. Versionado en Git, respaldado por CI/CD, y listo para compilarse en un PDF perfecto con un solo comando. Hemos logrado la inmutabilidad de la presencia digital.
6. Código Fuente y Demo
Si te interesa implementar esto sin todo el contexto de mi portafolio, he extraído la lógica principal en un repositorio independiente. Contiene el HTML base, el CSS de impresión y el script de Puppeteer + PDF-Lib listo para usar: