import type { RequestHandler } from './$types.js'; import type { LaunchOptions } from 'puppeteer'; import puppeteer from 'puppeteer'; import * as cheerio from 'cheerio'; import path from 'path'; import { opendir, writeFile, mkdtemp, copyFile } from 'fs/promises'; import { tmpdir } from 'os'; export const prerender = true; const cwd = process.cwd(); // PDF generation configuration const PDF_CONFIG = { format: 'A4' as const, printBackground: true, preferCSSPageSize: true, margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm', }, }; // Browser launch options for different environments const getLaunchOptions = (): LaunchOptions => { const chromePath = process.env.PUPPETEER_EXECUTABLE_PATH; const options: LaunchOptions = { headless: true, }; if (chromePath) { options.executablePath = chromePath; } return options; }; export const GET: RequestHandler = async ({ url, fetch }) => { const tmpDir = await mkdtemp(path.join(tmpdir(), 'cv-pdf-genration-')); const tmpFile = (url: string) => { const filename = path.basename(url); const tempFile = path.join(tmpDir, filename); return tempFile; }; try { // Launch browser const browser = await puppeteer.launch(getLaunchOptions()); const page = await browser.newPage(); const printResponse = await fetch('/print/'); const html = await printResponse.text(); const $ = cheerio.load(html); const fileDownloads: Record = {}; $('script[src], link[rel="stylesheet"], img[src]').each((i, el) => { if (el.tagName === 'link') { const href = $(el).attr('href'); if (href) { const tempFile = (fileDownloads[href] ||= tmpFile(href)); $(el).attr('href', `file://${tempFile}`); } } else { const src = $(el).attr('src'); if (src) { const tempFile = (fileDownloads[src] ||= tmpFile(src)); $(el).attr('src', `file://${tempFile}`); } } }); $('style[src]').each((i, el) => { const src = $(el).attr('src'); if (src) { const tempFile = (fileDownloads[src] ||= tmpFile(src)); $(el).attr('src', `file://${tempFile}`); } }); $('style:not([src])').each((i, el) => { const content = $(el).text(); $(el).text( content.replaceAll(/(?<=url\(".+?)(?=")/g, (match) => { const url = match[0]; const tempFile = (fileDownloads[url] ||= tmpFile(url)); return `file://${tempFile}`; }) ); }); const dir = await opendir('.svelte-kit/output/client/_app/immutable/assets'); for await (const file of dir) { await copyFile(path.join(file.parentPath, file.name), tmpFile(file.name)); } const htmlFile = path.join(tmpDir, 'index.html'); await writeFile(htmlFile, $.root().html() ?? ''); // Navigate to the PDF page with increased timeout // waitUntil: 'networkidle2' waits for 2 network connections to be idle // This is more lenient than 'networkidle0' which waits for 0 connections await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle2' }); // Wait for fonts to load await page.evaluateHandle('document.fonts.ready'); // Generate PDF const pdfBuffer = await page.pdf(PDF_CONFIG); // Close browser await browser.close(); // Create Blob from buffer for Response const pdfBlob = new Blob([Buffer.from(pdfBuffer).buffer], { type: 'application/pdf' }); // Return PDF with appropriate headers return new Response(pdfBlob, { status: 200, headers: { 'Content-Type': 'application/pdf', 'Cache-Control': 'public, max-age=3600', // Cache for 1 hour }, }); } catch (error) { console.error('PDF generation error:', error); return new Response( JSON.stringify({ error: 'Failed to generate PDF', message: error instanceof Error ? error.message : 'Unknown error', }), { status: 500, headers: { 'Content-Type': 'application/json' }, } ); } };