Files
2026-07-06 11:05:50 -04:00

9.4 KiB
Raw Permalink Blame History

Modo: pdf — Generación de PDF ATS-Optimizado

Pipeline completo

  1. Lee cv.md como fuentes de verdad
  2. Pide al usuario el JD si no está en contexto (texto o URL)
  3. Extrae 15-20 keywords del JD
  4. Detecta idioma del JD → idioma del CV (EN default)
  5. Detecta ubicación empresa → formato papel:
    • US/Canada → letter
    • Resto del mundo → a4
  6. Detecta arquetipo del rol → adapta framing
  7. Reescribe Professional Summary inyectando keywords del JD + exit narrative bridge ("Built and sold a business. Now applying systems thinking to [domain del JD].")
  8. Selecciona top 3-4 proyectos más relevantes para la oferta
  9. Reordena bullets de experiencia por relevancia al JD
  10. Construye competency grid desde requisitos del JD (6-8 keyword phrases)
  11. Inyecta keywords naturalmente en logros existentes (NUNCA inventa)
  12. Genera HTML completo desde template + contenido personalizado
  13. Lee name de config/profile.yml → normaliza a kebab-case lowercase (e.g. "John Doe" → "john-doe") → {candidate}
  14. Escribe HTML a /tmp/cv-{candidate}-{company}.html
  15. Ejecuta: node generate-pdf.mjs /tmp/cv-{candidate}-{company}.html output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf --format={letter|a4}
  16. Reporta: ruta del PDF, nº páginas, % cobertura de keywords

Reglas ATS (parseo limpio)

  • Layout single-column (sin sidebars, sin columnas paralelas)
  • Headers estándar: "Professional Summary", "Work Experience", "Education", "Skills", "Certifications", "Projects"
  • Sin texto en imágenes/SVGs
  • Sin info crítica en headers/footers del PDF (ATS los ignora)
  • UTF-8, texto seleccionable (no rasterizado)
  • Sin tablas anidadas
  • Keywords del JD distribuidas: Summary (top 5), primer bullet de cada rol, Skills section

Diseño del PDF

  • Fonts: Space Grotesk (headings, 600-700) + DM Sans (body, 400-500)
  • Fonts self-hosted: fonts/
  • Header: nombre en Space Grotesk 24px bold + línea gradiente linear-gradient(to right, hsl(187,74%,32%), hsl(270,70%,45%)) 2px + fila de contacto
  • Section headers: Space Grotesk 13px, uppercase, letter-spacing 0.05em, color cyan primary
  • Body: DM Sans 11px, line-height 1.5
  • Company names: color accent purple hsl(270,70%,45%)
  • Márgenes: 0.6in
  • Background: blanco puro

Orden de secciones (optimizado "6-second recruiter scan")

  1. Header (nombre grande, gradiente, contacto, link portfolio)
  2. Professional Summary (3-4 líneas, keyword-dense)
  3. Core Competencies (6-8 keyword phrases en flex-grid)
  4. Work Experience (cronológico inverso)
  5. Projects (top 3-4 más relevantes)
  6. Education & Certifications
  7. Skills (idiomas + técnicos)

Estrategia de keyword injection (ético, basado en verdad)

Ejemplos de reformulación legítima:

  • JD dice "RAG pipelines" y CV dice "LLM workflows with retrieval" → cambiar a "RAG pipeline design and LLM orchestration workflows"
  • JD dice "MLOps" y CV dice "observability, evals, error handling" → cambiar a "MLOps and observability: evals, error handling, cost monitoring"
  • JD dice "stakeholder management" y CV dice "collaborated with team" → cambiar a "stakeholder management across engineering, operations, and business"

NUNCA añadir skills que el candidato no tiene. Solo reformular experiencia real con el vocabulario exacto del JD.

Template HTML

Usar el template en cv-template.html. Reemplazar los placeholders {{...}} con contenido personalizado:

Placeholder Contenido
{{LANG}} en o es
{{PAGE_WIDTH}} 8.5in (letter) o 210mm (A4)
{{NAME}} (from profile.yml)
{{PHONE}} (from profile.yml — include with its separator only when profile.yml has a non-empty phone value; omit both <span> and <span class="separator"> otherwise)
{{EMAIL}} (from profile.yml)
{{LINKEDIN_URL}} [from profile.yml]
{{LINKEDIN_DISPLAY}} [from profile.yml]
{{PORTFOLIO_URL}} [from profile.yml] (o /es según idioma)
{{PORTFOLIO_DISPLAY}} [from profile.yml] (o /es según idioma)
{{LOCATION}} [from profile.yml]
{{SECTION_SUMMARY}} Professional Summary / Resumen Profesional
{{SUMMARY_TEXT}} Summary personalizado con keywords
{{SECTION_COMPETENCIES}} Core Competencies / Competencias Core
{{COMPETENCIES}} <span class="competency-tag">keyword</span> × 6-8
{{SECTION_EXPERIENCE}} Work Experience / Experiencia Laboral
{{EXPERIENCE}} HTML de cada trabajo con bullets reordenados
{{SECTION_PROJECTS}} Projects / Proyectos
{{PROJECTS}} HTML de top 3-4 proyectos
{{SECTION_EDUCATION}} Education / Formación
{{EDUCATION}} HTML de educación
{{SECTION_CERTIFICATIONS}} Certifications / Certificaciones
{{CERTIFICATIONS}} HTML de certificaciones
{{SECTION_SKILLS}} Skills / Competencias
{{SKILLS}} HTML de skills

Canva CV Generation (optional)

If config/profile.yml has canva_resume_design_id set, offer the user a choice before generating:

  • "HTML/PDF (fast, ATS-optimized)" — existing flow above
  • "Canva CV (visual, design-preserving)" — new flow below

If the user has no canva_resume_design_id, skip this prompt and use the HTML/PDF flow.

Canva workflow

Step 1 — Duplicate the base design

a. export-design the base design (using canva_resume_design_id) as PDF → get download URL b. import-design-from-url using that download URL → creates a new editable design (the duplicate) c. Note the new design_id for the duplicate

Step 2 — Read the design structure

a. get-design-content on the new design → returns all text elements (richtexts) with their content b. Map text elements to CV sections by content matching:

  • Look for the candidate's name → header section
  • Look for "Summary" or "Professional Summary" → summary section
  • Look for company names from cv.md → experience sections
  • Look for degree/school names → education section
  • Look for skill keywords → skills section c. If mapping fails, show the user what was found and ask for guidance

Step 3 — Generate tailored content

Same content generation as the HTML flow (Steps 1-11 above):

  • Rewrite Professional Summary with JD keywords + exit narrative
  • Reorder experience bullets by JD relevance
  • Select top competencies from JD requirements
  • Inject keywords naturally (NEVER invent)

IMPORTANT — Character budget rule: Each replacement text MUST be approximately the same length as the original text it replaces (within ±15% character count). If tailored content is longer, condense it. The Canva design has fixed-size text boxes — longer text causes overlapping with adjacent elements. Count the characters in each original element from Step 2 and enforce this budget when generating replacements.

Step 4 — Apply edits

a. start-editing-transaction on the duplicate design b. perform-editing-operations with find_and_replace_text for each section:

  • Replace summary text with tailored summary
  • Replace each experience bullet with reordered/rewritten bullets
  • Replace competency/skills text with JD-matched terms
  • Replace project descriptions with top relevant projects c. Reflow layout after text replacement: After applying all text replacements, the text boxes auto-resize but neighboring elements stay in place. This causes uneven spacing between work experience sections. Fix this:
  1. Read the updated element positions and dimensions from the perform-editing-operations response
  2. For each work experience section (top to bottom), calculate where the bullets text box ends: end_y = top + height
  3. The next section's header should start at end_y + consistent_gap (use the original gap from the template, typically ~30px)
  4. Use position_element to move the next section's date, company name, role title, and bullets elements to maintain even spacing
  5. Repeat for all work experience sections d. Verify layout before commit:
  • get-design-thumbnail with the transaction_id and page_index=1
  • Visually inspect the thumbnail for: text overlapping, uneven spacing, text cut off, text too small
  • If issues remain, adjust with position_element, resize_element, or format_text
  • Repeat until layout is clean d. Show the user the final preview and ask for approval e. commit-editing-transaction to save (ONLY after user approval)

Step 5 — Export and download PDF

a. export-design the duplicate as PDF (format: a4 or letter based on JD location) b. IMMEDIATELY download the PDF using Bash:

curl -sL -o "output/cv-{candidate}-{company}-canva-{YYYY-MM-DD}.pdf" "{download_url}"

The export URL is a pre-signed S3 link that expires in ~2 hours. Download it right away. c. Verify the download:

file output/cv-{candidate}-{company}-canva-{YYYY-MM-DD}.pdf

Must show "PDF document". If it shows XML or HTML, the URL expired — re-export and retry. d. Report: PDF path, file size, Canva design URL (for manual tweaking)

Error handling

  • If import-design-from-url fails → fall back to HTML/PDF pipeline with message
  • If text elements can't be mapped → warn user, show what was found, ask for manual mapping
  • If find_and_replace_text finds no matches → try broader substring matching
  • Always provide the Canva design URL so the user can edit manually if auto-edit fails

Post-generación

Actualizar tracker si la oferta ya está registrada: cambiar PDF de a .