How to Generate PDF from Your CV in Nuxt 3
In this tutorial, I’ll show you how to implement PDF generation for your CV using Nuxt 3 and Puppeteer. This approach allows you to create high-quality PDFs that maintain your CV’s styling and layout.
Prerequisites
- Nuxt 3 project
- Node.js installed
- Basic understanding of Vue.js and Nuxt 3
Step 1: Install Dependencies
First, install Puppeteer:
npm install puppeteer
Step 2: Create the PDF Generation API
Create a new file server/api/generate-pdf/[id].get.js
:
import puppeteer from 'puppeteer'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineEventHandler(async event => {
try {
const id = event.context.params.id
console.log('Generating PDF for ID:', id)
// Get current date and time for filename
const now = new Date()
const dateStr = now
.toISOString()
.replace(/[-:]/g, '')
.replace(/[T.]/g, '.')
.replace('Z', '')
.slice(0, 19)
// Get the project root directory
const tempDir = path.resolve(process.cwd(), '.temp')
const tempFile = path.join(tempDir, `${id}.json`)
if (!fs.existsSync(tempFile)) {
throw createError({
statusCode: 404,
message: 'Print content not found'
})
}
const data = JSON.parse(fs.readFileSync(tempFile, 'utf-8'))
// Launch Puppeteer
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
})
try {
const page = await browser.newPage()
// Set viewport to A4 size
await page.setViewport({
width: 794, // A4 width in pixels at 96 DPI
height: 1123 // A4 height in pixels at 96 DPI
})
// Set content type and encoding
await page.setExtraHTTPHeaders({
'Content-Type': 'text/html; charset=utf-8'
})
// Get the base URL from request headers
const protocol = event.node.req.headers['x-forwarded-proto'] || 'http'
const host = event.node.req.headers.host
const baseUrl = `${protocol}://${host}`
// Navigate to the print-content API endpoint
const printContentUrl = `${baseUrl}/api/print-content/${id}`
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))
// Generate PDF
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '10mm',
right: '10mm',
bottom: '10mm',
left: '10mm'
},
preferCSSPageSize: true,
displayHeaderFooter: false,
timeout: 30000
})
// Set headers for PDF download
setHeader(event, 'Content-Type', 'application/pdf')
setHeader(
event,
'Content-Disposition',
`attachment; filename="cv-${dateStr}.pdf"`
)
return pdfBuffer
} finally {
await browser.close()
}
} catch (error) {
console.error('Error generating PDF:', error)
throw createError({
statusCode: error.statusCode || 500,
message: `Failed to generate PDF: ${error.message}`
})
}
})
Step 3: Create the Print Content API
Create a new file server/api/print-content/[id].get.js
:
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineEventHandler(async event => {
try {
const id = event.context.params.id
const tempDir = path.resolve(process.cwd(), '.temp')
const tempFile = path.join(tempDir, `${id}.json`)
if (!fs.existsSync(tempFile)) {
throw createError({
statusCode: 404,
message: 'Print content not found'
})
}
const data = JSON.parse(fs.readFileSync(tempFile, 'utf-8'))
// Create HTML with content and styles
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
${data.styles || ''}
@page {
size: A4;
margin: 20mm;
}
body {
margin: 0;
padding: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
background: white;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.print {
width: 210mm;
min-height: 297mm;
padding: 0;
margin: 0 auto;
background: white;
box-sizing: border-box;
}
* {
box-sizing: border-box;
}
</style>
</head>
<body>
<div class="print">
${data.content || ''}
</div>
</body>
</html>
`
setHeader(event, 'Content-Type', 'text/html')
return html
} catch (error) {
console.error('Error reading print content:', error)
throw createError({
statusCode: error.statusCode || 500,
message: `Failed to retrieve print content: ${error.message}`
})
}
})
Step 4: Create the Temp Content API
Create a new file server/api/temp-content.post.js
:
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineEventHandler(async event => {
try {
const body = await readBody(event)
const tempId = new Date()
.toISOString()
.replace(/[-:]/g, '')
.replace(/[T.]/g, '.')
.replace('Z', '')
.slice(0, 19)
// Get the project root directory
const tempDir = path.resolve(process.cwd(), '.temp')
// Create temp directory if it doesn’t exist
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir)
}
// Save content to temp file
const tempFile = path.join(tempDir, `${tempId}.json`)
fs.writeFileSync(tempFile, JSON.stringify(body))
return { tempId }
} catch (error) {
console.error('Error saving temp content:', error)
throw createError({
statusCode: error.statusCode || 500,
message: `Failed to save temp content: ${error.message}`
})
}
})
Step 5: Implement the Download Function
In your CV component (e.g., components/layout/edit.vue
), add the download function:
async onDownloadPDF() {
try {
// Store current edit mode
const wasEditing = this.editMode
// Switch to view mode for PDF generation
this.editMode = false
// Wait for view mode to render
await this.$nextTick()
// Get content to print
const contentToPrint = document.querySelector('.print')
if (!contentToPrint) {
throw new Error('Print content not found')
}
// Get the full HTML structure including the print element itself
const content = contentToPrint.outerHTML
// 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)
}
}
// Create content object
const contentData = {
content,
styles,
computedStyles
}
// Upload content to temp storage
const response = await fetch('/api/temp-content', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(contentData)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.message || 'Failed to upload content')
}
const { tempId } = await response.json()
// Get current date and time for filename
const now = new Date()
const dateStr = now
.toISOString()
.replace(/[-:]/g, '')
.replace(/[T.]/g, '.')
.replace('Z', '')
.slice(0, 19)
// Generate PDF
const pdfResponse = await fetch(`/api/generate-pdf/${tempId}`)
if (!pdfResponse.ok) {
const errorData = await pdfResponse.json()
throw new Error(errorData.message || 'Failed to generate PDF')
}
// Download the PDF
const pdfBlob = await pdfResponse.blob()
const url = window.URL.createObjectURL(pdfBlob)
const link = document.createElement('a')
link.href = url
link.download = `cv-${dateStr}.pdf`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
// Restore edit mode
this.editMode = wasEditing
} catch (error) {
console.error('Error preparing PDF:', error)
alert(`Error preparing PDF: ${error.message}`)
}
}
How It Works
When the user clicks the download button:
- The current content and styles are captured
- The content is saved to a temporary file
- A unique ID is generated for the content
The PDF generation process:
- Puppeteer launches a headless browser
- Sets the viewport to A4 size
- Loads the content with styles
- Generates a PDF with proper margins and encoding
- Downloads the PDF with a timestamped filename
Key features:
- Maintains all styles and layout
- Proper character encoding
- A4 format with custom margins
- Timestamped filenames
- Error handling
Tips for Best Results
- Use a
.print
class for your CV content container - Ensure all styles are properly scoped
- Test with different content lengths
- Check character encoding for special characters
- Adjust margins as needed
Conclusion
This implementation provides a robust solution for generating PDFs from your CV in Nuxt 3. It maintains styling, handles special characters, and provides a good user experience with proper error handling.
Remember to:
- Test thoroughly with different content
- Adjust margins and dimensions as needed
- Handle errors gracefully
- Clean up temporary files if needed
Happy coding! 🚀