Files
portfolio/server/api/resume/pdf.get.ts
T
mahdiarghyani 12c8847447 feat(resume): enhance spacing, styling, and add markdown text parsing
- Update section spacing from mb-8 to mb-12 for improved visual hierarchy
- Refine heading padding and margins for consistent section styling
- Adjust work experience highlights spacing and add leading-relaxed for readability
- Implement markdown bold text parsing in job highlights using new composable
- Add color-coded icons in resume header (red for location, green for phone, amber for email, blue for website)
- Reorganize contact information into two lines for better layout organization
- Standardize icon sizing with explicit w-4 h-4 dimensions across header
- Create useMarkdownText composable for markdown formatting utilities
- Update resume data and PDF export endpoint to support enhanced styling
- Improve print styling consistency across all resume components
2025-12-06 14:04:45 +03:30

186 lines
5.1 KiB
TypeScript

import puppeteer from 'puppeteer'
import type { Browser } from 'puppeteer'
export default defineEventHandler(async (event) => {
let browser: Browser | null = null
try {
const requestUrl = getRequestURL(event)
const host = requestUrl.host.includes('192.168') || requestUrl.host.includes('localhost')
? 'localhost:5000'
: requestUrl.host
const baseUrl = `http://${host}`
const resumeUrl = `${baseUrl}/resume?print=true`
console.log('[PDF API] Generating from:', resumeUrl)
const isDev = process.env.NODE_ENV === 'development'
if (isDev) {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
})
} else {
const chromium = await import('@sparticuz/chromium')
const puppeteerCore = await import('puppeteer-core')
browser = await puppeteerCore.default.launch({
args: chromium.default.args,
executablePath: await chromium.default.executablePath(),
headless: true,
})
}
const page = await browser.newPage()
// Set viewport to A4 dimensions (210mm x 297mm at 96 DPI)
await page.setViewport({
width: 794, // 210mm
height: 1123 // 297mm
})
// Enable print media type BEFORE loading page
await page.emulateMediaType('print')
const response = await page.goto(resumeUrl, {
waitUntil: 'networkidle0',
timeout: 30000,
})
if (!response || !response.ok()) {
throw new Error(`Failed to load: ${response?.status()}`)
}
// Wait for Vue hydration and v-html rendering to complete
await page.waitForSelector('strong', { timeout: 5000 }).catch(() => {
console.log('[PDF API] No strong tags found, continuing anyway')
})
// Additional wait to ensure all dynamic content is rendered
await new Promise(resolve => setTimeout(resolve, 1000))
// Inject critical CSS fixes for PDF generation - optimized for exactly 2 pages
await page.addStyleTag({
content: `
@page {
size: A4;
margin: 1cm;
}
* {
box-shadow: none !important;
word-break: normal !important;
hyphens: none !important;
-webkit-hyphens: none !important;
-ms-hyphens: none !important;
print-color-adjust: exact !important;
-webkit-print-color-adjust: exact !important;
}
html, body {
margin: 0 !important;
padding: 0 !important;
background: white !important;
min-height: auto !important;
}
.resume-wrapper {
background: white !important;
padding: 0 !important;
display: block !important;
margin: 0 !important;
}
.resume-container {
box-shadow: none !important;
max-width: none !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
.resume-content {
padding: 1rem !important;
}
/* Balanced spacing for sections - fits exactly 2 pages */
section {
margin-bottom: 1.5rem !important;
page-break-inside: auto !important;
break-inside: auto !important;
}
section:last-child {
margin-bottom: 0 !important;
}
/* Section titles */
section h2 {
margin-bottom: 0.7rem !important;
padding-bottom: 0.3rem !important;
page-break-after: avoid !important;
break-after: avoid !important;
}
/* Work experience job blocks */
section > div {
margin-bottom: 1rem !important;
}
section > div:last-child {
margin-bottom: 0 !important;
}
/* Bullet lists */
ul {
margin-top: 0.5rem !important;
}
ul li {
margin-bottom: 0.25rem !important;
line-height: 1.45 !important;
}
strong {
font-weight: 700 !important;
}
/* Summary paragraph - more line height */
section > p {
line-height: 1.75 !important;
}
`,
})
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: 0, right: 0, bottom: 0, left: 0 },
preferCSSPageSize: true,
})
const query = getQuery(event)
const filename = (query.filename as string) || 'Ali_Arghyani_Resume.pdf'
const download = query.download === 'true'
setResponseHeaders(event, {
'Content-Type': 'application/pdf',
// inline = show in browser, attachment = force download
'Content-Disposition': `${download ? 'attachment' : 'inline'}; filename="${filename}"`,
})
return pdf
} catch (error) {
console.error('PDF generation failed:', error)
setResponseStatus(event, 500)
return {
error: 'PDF generation failed',
message: error instanceof Error ? error.message : 'Unknown error',
}
} finally {
if (browser) {
await browser.close()
}
}
})