diff --git a/package-lock.json b/package-lock.json index 34ddfb4..d55d8e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@fontsource-variable/roboto-condensed": "^5.2.8", "@fontsource/iosevka": "^5.2.5", "@sveltia/cms": "^0.140.3" }, @@ -17,6 +18,7 @@ "@sveltejs/kit": "^2.51.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.1.18", + "cheerio": "^1.2.0", "dotenv": "^17.3.1", "http-server": "^14.1.1", "js-yaml": "^4.1.1", @@ -498,6 +500,15 @@ "node": ">=18" } }, + "node_modules/@fontsource-variable/roboto-condensed": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/roboto-condensed/-/roboto-condensed-5.2.8.tgz", + "integrity": "sha512-aIZ2kYSoJHkTI4z8x/PRgKX6Zb9TTtSE/u+fUYeiwL+5trP9rhYYEEeNjRttaMqRgoDHcSueArdRZ43wf/i2Kw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@fontsource/iosevka": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/@fontsource/iosevka/-/iosevka-5.2.5.tgz", @@ -1604,6 +1615,13 @@ "node": ">=10.0.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1672,6 +1690,50 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1794,6 +1856,36 @@ } } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -1871,6 +1963,65 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -1906,6 +2057,34 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -1930,6 +2109,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -2386,6 +2578,39 @@ "node": ">=12" } }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -2952,6 +3177,19 @@ "node": ">= 0.4.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -3062,6 +3300,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -3785,6 +4076,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -3928,6 +4229,16 @@ "node": ">=12" } }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index ebb6712..3a33726 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "private": true, "scripts": { "dev": "vite dev", - "build": "vite build && node scripts/generate-pdf.js", + "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "format": "prettier --write ." }, "dependencies": { + "@fontsource-variable/roboto-condensed": "^5.2.8", "@fontsource/iosevka": "^5.2.5", "@sveltia/cms": "^0.140.3" }, @@ -28,6 +29,7 @@ "@sveltejs/kit": "^2.51.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.1.18", + "cheerio": "^1.2.0", "dotenv": "^17.3.1", "http-server": "^14.1.1", "js-yaml": "^4.1.1", diff --git a/scripts/generate-pdf.js b/scripts/generate-pdf.js deleted file mode 100644 index 6cad5c3..0000000 --- a/scripts/generate-pdf.js +++ /dev/null @@ -1,117 +0,0 @@ -import puppeteer from 'puppeteer'; -import httpServer from 'http-server'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; -import { writeFile } from 'fs/promises'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const BUILD_DIR = join(__dirname, '..', 'build'); -const OUTPUT_PDF = join(BUILD_DIR, 'joakim-repomaa-cv.pdf'); - -async function generatePDF() { - console.log('🚀 Starting PDF generation...'); - - // Check if we're in an environment that can run Chrome - const isCI = process.env.CI || process.env.CONTINUOUS_INTEGRATION; - const chromePath = process.env.PUPPETEER_EXECUTABLE_PATH; - - if (chromePath) { - console.log('🔍 Using Chrome from:', chromePath); - } - - // Start static server - console.log('📡 Starting static file server...'); - const server = httpServer.createServer({ - root: BUILD_DIR, - port: 3456, - cache: -1, // Disable caching - cors: true, - }); - - await new Promise((resolve) => { - server.listen(3456, 'localhost', () => { - console.log('✅ Server running at http://localhost:3456'); - resolve(); - }); - }); - - try { - // Launch browser options - const launchOptions = { - headless: true, - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-accelerated-2d-canvas', - '--disable-gpu', - ], - }; - - // Use custom Chrome path if available (e.g., from devbox or CI) - if (chromePath) { - launchOptions.executablePath = chromePath; - } - - // Launch browser - console.log('🌐 Launching browser...'); - const browser = await puppeteer.launch(launchOptions); - - const page = await browser.newPage(); - - // Navigate to print route for HTML rendering - console.log('📄 Loading CV page...'); - await page.goto('http://localhost:3456/print/', { - waitUntil: 'networkidle0', - timeout: 30000, - }); - - // Wait for fonts to load - await page.evaluateHandle('document.fonts.ready'); - - // Generate PDF - console.log('📝 Generating PDF...'); - const pdfBuffer = await page.pdf({ - format: 'A4', - printBackground: true, - preferCSSPageSize: true, - margin: { - top: '20mm', - right: '20mm', - bottom: '20mm', - left: '20mm', - }, - }); - - // Save PDF - await writeFile(OUTPUT_PDF, pdfBuffer); - console.log('✅ PDF saved to:', OUTPUT_PDF); - - // Close browser - await browser.close(); - } catch (error) { - console.error('⚠️ Error generating PDF:', error.message); - - if (isCI) { - console.log('📝 CI environment detected, but Chrome may not be available.'); - console.log(' The PDF will need to be generated in a post-deployment step'); - console.log(' or in a CI environment with Chrome/Chromium installed.'); - } else { - console.log('📝 To generate the PDF:'); - console.log(' Option 1: Run in devbox (NixOS): devbox run npm run build'); - console.log(' Option 2: Deploy to a hosting platform with Chrome'); - } - - // Don't exit with error - the static site is still usable - console.log('⚠️ Continuing without PDF...'); - } finally { - // Stop server - server.close(); - console.log('🛑 Server stopped'); - } - - console.log('🎉 Build complete!'); -} - -generatePDF(); diff --git a/src/app.css b/src/app.css index 88cb1b5..6ed32dc 100644 --- a/src/app.css +++ b/src/app.css @@ -3,6 +3,7 @@ /* Iosevka font - self-hosted via npm */ @import '@fontsource/iosevka/400.css'; @import '@fontsource/iosevka/700.css'; +@import '@fontsource-variable/roboto-condensed'; html { color-scheme: light dark; @@ -17,7 +18,7 @@ body { @theme { /* Font families */ --font-mono: 'Iosevka', ui-monospace, monospace; - --font-sans: ui-sans-serif, system-ui, sans-serif; + --font-sans: 'Roboto Condensed Variable', ui-sans-serif, sans-serif; /* Web layout - 5 colors using light-dark() for dark mode */ --color-bg: light-dark(#fafafa, #0c0c0e); diff --git a/src/lib/components/PDFTags.svelte b/src/lib/components/PDFTags.svelte index 5bf0054..9c4aeeb 100644 --- a/src/lib/components/PDFTags.svelte +++ b/src/lib/components/PDFTags.svelte @@ -7,9 +7,9 @@ {#if tags.length > 0} -
+
{#each tags as tag} - {tag} + {tag} {/each}
{/if} diff --git a/src/routes/+layout.svelte b/src/routes/(web)/+layout.svelte similarity index 92% rename from src/routes/+layout.svelte rename to src/routes/(web)/+layout.svelte index 5b11c32..4cf2e66 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/(web)/+layout.svelte @@ -1,6 +1,6 @@

- + Edit Content →

diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..ba58d86 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,2 @@ +export const prerender = true; +export const trailingSlash = 'always'; diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts deleted file mode 100644 index a3d1578..0000000 --- a/src/routes/admin/+page.server.ts +++ /dev/null @@ -1 +0,0 @@ -export const ssr = false; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index efdd747..e12c3c6 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -1,102 +1,106 @@ diff --git a/src/routes/joakim-repomaa-cv.pdf/+server.ts b/src/routes/joakim-repomaa-cv.pdf/+server.ts index 8e2f58a..cc67ca1 100644 --- a/src/routes/joakim-repomaa-cv.pdf/+server.ts +++ b/src/routes/joakim-repomaa-cv.pdf/+server.ts @@ -1,9 +1,14 @@ 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'; -// This is a dynamic endpoint - not prerenderable -export const prerender = false; +export const prerender = true; + +const cwd = process.cwd(); // PDF generation configuration const PDF_CONFIG = { @@ -23,13 +28,6 @@ const getLaunchOptions = (): LaunchOptions => { const chromePath = process.env.PUPPETEER_EXECUTABLE_PATH; const options: LaunchOptions = { headless: true, - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-accelerated-2d-canvas', - '--disable-gpu', - ], }; if (chromePath) { options.executablePath = chromePath; @@ -37,43 +35,71 @@ const getLaunchOptions = (): LaunchOptions => { return options; }; -export const GET: RequestHandler = async ({ url }: { url: URL }) => { - // Check if Chrome/Puppeteer is available - const chromePath = process.env.PUPPETEER_EXECUTABLE_PATH; - const isCI = process.env.CI || process.env.CONTINUOUS_INTEGRATION; - - if (!chromePath && !isCI) { - // Return 503 if Chrome is not available - return new Response( - JSON.stringify({ - error: 'PDF generation not available', - message: - 'Chrome/Chromium is not configured. Set PUPPETEER_EXECUTABLE_PATH environment variable.', - }), - { - status: 503, - headers: { 'Content-Type': 'application/json' }, - } - ); - } +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 { - // Get the base URL for the current request - const baseUrl = `${url.protocol}//${url.host}`; - const pdfUrl = `${baseUrl}/print/`; - // 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(pdfUrl, { - waitUntil: 'networkidle2', - timeout: 120000, // Increased from 30s to 120s to handle slow GitHub API calls - }); - + await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle2' }); // Wait for fonts to load await page.evaluateHandle('document.fonts.ready'); @@ -84,7 +110,7 @@ export const GET: RequestHandler = async ({ url }: { url: URL }) => { await browser.close(); // Create Blob from buffer for Response - const pdfBlob = new Blob([pdfBuffer as unknown as ArrayBuffer], { type: 'application/pdf' }); + const pdfBlob = new Blob([Buffer.from(pdfBuffer).buffer], { type: 'application/pdf' }); // Return PDF with appropriate headers return new Response(pdfBlob, { diff --git a/src/routes/print/+layout.svelte b/src/routes/print/+layout.svelte new file mode 100644 index 0000000..be033bd --- /dev/null +++ b/src/routes/print/+layout.svelte @@ -0,0 +1,6 @@ + + +{@render children()} diff --git a/svelte.config.js b/svelte.config.js index 2585746..a48bfa5 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -5,26 +5,7 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; const config = { preprocess: vitePreprocess(), kit: { - adapter: adapter({ - pages: 'build', - assets: 'build', - precompress: false, - strict: false, - }), - prerender: { - entries: ['/', '/print'], - handleHttpError: ({ path, message }) => { - // Ignore 404s for cv.pdf (generated after build) and static files - if (path === '/cv.pdf' || path.startsWith('/admin/')) { - return; - } - console.warn(`Prerender error for ${path}: ${message}`); - }, - handleMissingId: () => { - // Ignore missing IDs for hash links - return; - }, - }, + adapter: adapter({}), alias: { $lib: './src/lib', },