fix pdf building
This commit is contained in:
311
package-lock.json
generated
311
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
</script>
|
||||
|
||||
{#if tags.length > 0}
|
||||
<div class="text-xs text-pdf-muted">
|
||||
<div class="text-xs text-pdf-muted flex flex-wrap gap-0.5">
|
||||
{#each tags as tag}
|
||||
<span class="first:before:content-[''] before:content-['_•_']">{tag}</span>
|
||||
<span class="first:before:content-[''] before:content-['_•_'] whitespace-nowrap">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
import '../app.css';
|
||||
import '../../app.css';
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -54,7 +54,7 @@
|
||||
Built with SvelteKit + Sveltia CMS
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
<a href="/admin/index.html" class="text-accent hover:text-accent/80 transition-colors">
|
||||
<a href="/admin" class="text-accent hover:text-accent/80 transition-colors">
|
||||
Edit Content →
|
||||
</a>
|
||||
</p>
|
||||
2
src/routes/+layout.server.ts
Normal file
2
src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
||||
@@ -1 +0,0 @@
|
||||
export const ssr = false;
|
||||
@@ -1,102 +1,106 @@
|
||||
<script lang="ts">
|
||||
import CMS from '@sveltia/cms';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
CMS.init({
|
||||
config: {
|
||||
load_config_file: false,
|
||||
media_folder: 'src/lib/media',
|
||||
backend: {
|
||||
name: 'gitea',
|
||||
app_id: 'a046b53c-787a-4b76-bd3a-633221a38954',
|
||||
repo: 'repomaa/cv',
|
||||
onMount(async () => {
|
||||
const CMS = await import('@sveltia/cms');
|
||||
|
||||
CMS.init({
|
||||
config: {
|
||||
load_config_file: false,
|
||||
media_folder: 'src/lib/media',
|
||||
backend: {
|
||||
name: 'gitea',
|
||||
app_id: 'a046b53c-787a-4b76-bd3a-633221a38954',
|
||||
repo: 'repomaa/cv',
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
name: 'profile',
|
||||
label: 'Profile',
|
||||
format: 'json',
|
||||
files: [
|
||||
{
|
||||
name: 'profile',
|
||||
label: 'Profile',
|
||||
file: 'src/lib/content/profile.json',
|
||||
fields: [
|
||||
{ name: 'name', label: 'Name', widget: 'string' },
|
||||
{ name: 'title', label: 'Title', widget: 'string' },
|
||||
{ name: 'email', label: 'Email', widget: 'string' },
|
||||
{ name: 'phone', label: 'Phone', widget: 'string', required: false },
|
||||
{ name: 'location', label: 'Location', widget: 'string' },
|
||||
{ name: 'website', label: 'Website', widget: 'string', required: false },
|
||||
{ name: 'github', label: 'GitHub', widget: 'string' },
|
||||
{ name: 'summary', label: 'Summary', widget: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'experience',
|
||||
label: 'Experience',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/experience',
|
||||
fields: [
|
||||
{ name: 'company', label: 'Company', widget: 'string' },
|
||||
{ name: 'position', label: 'Position', widget: 'string' },
|
||||
{ name: 'location', label: 'Location', widget: 'string', required: false },
|
||||
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
widget: 'datetime',
|
||||
time_format: false,
|
||||
required: false,
|
||||
},
|
||||
{ name: 'description', label: 'Description', widget: 'markdown' },
|
||||
{
|
||||
name: 'technologies',
|
||||
label: 'Technologies',
|
||||
widget: 'list',
|
||||
field: { label: 'Technology', widget: 'string' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'education',
|
||||
label: 'Education',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/education',
|
||||
fields: [
|
||||
{ name: 'institution', label: 'Institution', widget: 'string' },
|
||||
{ name: 'degree', label: 'Degree', widget: 'string' },
|
||||
{ name: 'field', label: 'Field', widget: 'string', required: false },
|
||||
{ name: 'location', label: 'Location', widget: 'string', required: false },
|
||||
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
widget: 'datetime',
|
||||
time_format: false,
|
||||
required: false,
|
||||
},
|
||||
{ name: 'description', label: 'Description', widget: 'markdown', required: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'skills',
|
||||
label: 'Skills',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/skills',
|
||||
fields: [
|
||||
{ name: 'category', label: 'Category', widget: 'string' },
|
||||
{
|
||||
name: 'items',
|
||||
label: 'Skills',
|
||||
widget: 'list',
|
||||
field: { label: 'Skill', widget: 'string' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
name: 'profile',
|
||||
label: 'Profile',
|
||||
format: 'json',
|
||||
files: [
|
||||
{
|
||||
name: 'profile',
|
||||
label: 'Profile',
|
||||
file: 'src/lib/content/profile.json',
|
||||
fields: [
|
||||
{ name: 'name', label: 'Name', widget: 'string' },
|
||||
{ name: 'title', label: 'Title', widget: 'string' },
|
||||
{ name: 'email', label: 'Email', widget: 'string' },
|
||||
{ name: 'phone', label: 'Phone', widget: 'string', required: false },
|
||||
{ name: 'location', label: 'Location', widget: 'string' },
|
||||
{ name: 'website', label: 'Website', widget: 'string', required: false },
|
||||
{ name: 'github', label: 'GitHub', widget: 'string' },
|
||||
{ name: 'summary', label: 'Summary', widget: 'text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'experience',
|
||||
label: 'Experience',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/experience',
|
||||
fields: [
|
||||
{ name: 'company', label: 'Company', widget: 'string' },
|
||||
{ name: 'position', label: 'Position', widget: 'string' },
|
||||
{ name: 'location', label: 'Location', widget: 'string', required: false },
|
||||
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
widget: 'datetime',
|
||||
time_format: false,
|
||||
required: false,
|
||||
},
|
||||
{ name: 'description', label: 'Description', widget: 'markdown' },
|
||||
{
|
||||
name: 'technologies',
|
||||
label: 'Technologies',
|
||||
widget: 'list',
|
||||
field: { label: 'Technology', widget: 'string' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'education',
|
||||
label: 'Education',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/education',
|
||||
fields: [
|
||||
{ name: 'institution', label: 'Institution', widget: 'string' },
|
||||
{ name: 'degree', label: 'Degree', widget: 'string' },
|
||||
{ name: 'field', label: 'Field', widget: 'string', required: false },
|
||||
{ name: 'location', label: 'Location', widget: 'string', required: false },
|
||||
{ name: 'startDate', label: 'Start Date', widget: 'datetime', time_format: false },
|
||||
{
|
||||
name: 'endDate',
|
||||
label: 'End Date',
|
||||
widget: 'datetime',
|
||||
time_format: false,
|
||||
required: false,
|
||||
},
|
||||
{ name: 'description', label: 'Description', widget: 'markdown', required: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'skills',
|
||||
label: 'Skills',
|
||||
format: 'json',
|
||||
folder: 'src/lib/content/skills',
|
||||
fields: [
|
||||
{ name: 'category', label: 'Category', widget: 'string' },
|
||||
{
|
||||
name: 'items',
|
||||
label: 'Skills',
|
||||
widget: 'list',
|
||||
field: { label: 'Skill', widget: 'string' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
|
||||
$('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, {
|
||||
|
||||
6
src/routes/print/+layout.svelte
Normal file
6
src/routes/print/+layout.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
import '../../app.css';
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user