0 / 0
Skip to content

My Journey to Perfect PDF Generation for My CV

20 March 2025 - Rijswijk, Netherlands

It was a sunny but cold afternoon in Rijswijk when I decided to tackle one of the most frustrating aspects of my CV builder project: PDF generation. The browser’s built-in print-to-PDF just wasn’t cutting it anymore. My CV looked perfect on screen but turned into a mess when printed. Something had to change.

The Problem

I remember staring at my screen, watching my carefully crafted CV layout crumble when I tried to save it as PDF. The margins were wrong, some styles were missing, and special characters looked like they were from another planet. It was like watching a beautiful painting being photocopied through a dirty lens.

The Discovery

After some research, I stumbled upon Puppeteer. "A headless Chrome browser?" I thought. "That’s exactly what I need!" The idea of having a browser render my CV exactly as I see it on screen, but in PDF format, was too good to be true.

The Implementation Journey

First Attempt

I started with the basics. Install Puppeteer, create an API endpoint, and try to generate a PDF. Simple, right? Not quite. My first attempt resulted in a blank page. "Why?" I wondered. After some debugging, I realised I needed to capture not just the content, but all the styles too.

The Style Challenge

This was where things got interesting. I spent hours trying to figure out how to capture all the styles. The solution? A combination of styleSheets, cssRules, and computedStyles. It felt like I was building a time machine, capturing a moment in time when my CV looked perfect.

The real challenge came when dealing with dynamic styles. Some styles were being applied through JavaScript, others through CSS-in-JS, and some through external stylesheets. I had to create a comprehensive style capture system:

javascript
// Get all styles
const styles = Array.from(document.styleSheets)
  .map(sheet => {
    try {
      return Array.from(sheet.cssRules)
        .map(rule => rule.cssText)
        .join('\n')
    } catch (e) {
      console.warn('Could not read stylesheet:', e)
      return ''
    }
  })
  .join('\n')

// Get computed styles for each element
const computedStyles = {}
const elements = contentToPrint.getElementsByTagName('*')
for (const element of elements) {
  const style = window.getComputedStyle(element)
  const elementId = element.id || element.className || element.tagName.toLowerCase()
  computedStyles[elementId] = {}
  for (const prop of style) {
    computedStyles[elementId][prop] = style.getPropertyValue(prop)
  }
}

The Temporary Storage Solution

Then came the question: "How do I pass all this data to the server?" I needed a temporary storage solution. That’s when I created the .temp directory and started saving JSON files with timestamps. It felt like leaving breadcrumbs in the forest, making sure I could find my way back to the perfect version of my CV.

The challenge here was handling concurrent requests. What if multiple users were generating PDFs at the same time? I solved this by using unique timestamps in the filenames:

javascript
const tempId = new Date()
  .toISOString()
  .replace(/[-:]/g, '')
  .replace(/[T.]/g, '.')
  .replace('Z', '')
  .slice(0, 19)

The A4 Conundrum

The most challenging part was getting the A4 dimensions right. After some research, I learned that A4 at 96 DPI is 794x1123 pixels. This was a game-changer. Suddenly, my PDFs started looking exactly like my screen version.

But there was more to it than just dimensions. I had to handle:

  • Page breaks
  • Margins
  • Padding
  • Background colors
  • Font rendering

Here’s how I configured Puppeteer for perfect A4 output:

javascript
await page.setViewport({
  width: 794,  // A4 width in pixels at 96 DPI
  height: 1123 // A4 height in pixels at 96 DPI
})

const pdfBuffer = await page.pdf({
  format: 'A4',
  printBackground: true,
  margin: {
    top: '10mm',
    right: '10mm',
    bottom: '10mm',
    left: '10mm'
  },
  preferCSSPageSize: true,
  displayHeaderFooter: false,
  timeout: 30000
})

The Character Encoding Mystery

One of the most frustrating issues was with special characters. They would look perfect on screen but turn into gibberish in the PDF. The solution? Proper UTF-8 encoding and content type headers:

javascript
// In the HTML template
<meta charset="UTF-8">

// In the Puppeteer configuration
await page.setExtraHTTPHeaders({
  'Content-Type': 'text/html; charset=utf-8'
})

The Dynamic Content Challenge

Another hurdle was handling dynamic content. Sometimes the PDF would generate before all content was fully loaded. I solved this with a combination of waiting strategies:

javascript
// Wait for network to be idle
await page.goto(printContentUrl, { waitUntil: 'networkidle0' })

// Wait for content to be rendered
await page.waitForSelector('.print', { timeout: 5000 })

// Wait a bit for any dynamic content
await new Promise(resolve => setTimeout(resolve, 1000))

The Breakthrough

The moment of truth came when I successfully generated my first PDF. The margins were perfect (10mm all around), the styles were preserved, and even the special characters looked correct. It was like watching a magic trick unfold before my eyes.

The Final Touches

I added some nice touches:

  • Timestamped filenames (because who doesn’t love organisation?)
  • Proper error handling (because Murphy’s Law is always watching)
  • Clean temporary file management (because nobody likes a messy workspace)

What I Learned

This journey taught me several valuable lessons:

  1. Sometimes the simplest solution isn’t the best one
  2. Attention to detail matters (especially with PDFs)
  3. Temporary storage can be elegant
  4. Error handling is not optional
  5. Character encoding is more important than you think
  6. Multiple waiting strategies are better than one
  7. Concurrent request handling is crucial
  8. Proper cleanup is as important as proper setup

The Result

Now, when I click the download button, I get a perfect PDF of my CV. It’s like having a professional printer in my pocket. The margins are consistent, the styles are preserved, and everything looks exactly as it should.

Looking Forward

I’m already thinking about improvements:

  • Maybe add different paper sizes?
  • Custom margin presets?
  • Watermark support?
  • Multiple page handling?
  • PDF compression options?
  • Custom font embedding?
  • Interactive PDF features?

But for now, I’m happy with what I’ve achieved. It’s not just about generating PDFs anymore; it’s about creating a professional, reliable solution that I can be proud of.

To be continued...


Note: This implementation is now part of my open-source CV builder project. Feel free to check it out and contribute!