diff --git a/package.json b/package.json index d4e87dab9..6e0d6dec8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "generate-renderer": "sh scripts/generate-renderer.sh", "best-practice-dirs": "node scripts/best-practice-dirs.cjs", "best-practice-content": "node scripts/best-practice-content.cjs", + "generate:og": "node ./scripts/generate-og-images.mjs", "test:e2e": "playwright test" }, "dependencies": { @@ -27,6 +28,7 @@ "@astrojs/tailwind": "^5.1.0", "@fingerprintjs/fingerprintjs": "^4.2.2", "@nanostores/react": "^0.7.1", + "@resvg/resvg-js": "^2.6.0", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "astro": "^4.4.0", @@ -34,6 +36,9 @@ "clsx": "^2.1.0", "dom-to-image": "^2.6.0", "dracula-prism": "^2.1.16", + "gray-matter": "^4.0.3", + "htm": "^3.1.1", + "image-size": "^1.1.1", "jose": "^5.2.2", "js-cookie": "^3.0.5", "lucide-react": "^0.334.0", @@ -49,6 +54,9 @@ "rehype-external-links": "^3.0.0", "remark-parse": "^11.0.0", "roadmap-renderer": "^1.0.6", + "satori": "^0.10.13", + "satori-html": "^0.3.2", + "sharp": "^0.33.2", "slugify": "^1.6.6", "tailwind-merge": "^2.2.1", "tailwindcss": "^3.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40919589e..08ac06715 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@nanostores/react': specifier: ^0.7.1 version: 0.7.1(nanostores@0.9.5)(react@18.2.0) + '@resvg/resvg-js': + specifier: ^2.6.0 + version: 2.6.0 '@types/react': specifier: ^18.2.56 version: 18.2.59 @@ -41,6 +44,15 @@ dependencies: dracula-prism: specifier: ^2.1.16 version: 2.1.16 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 + htm: + specifier: ^3.1.1 + version: 3.1.1 + image-size: + specifier: ^1.1.1 + version: 1.1.1 jose: specifier: ^5.2.2 version: 5.2.2 @@ -86,6 +98,15 @@ dependencies: roadmap-renderer: specifier: ^1.0.6 version: 1.0.6 + satori: + specifier: ^0.10.13 + version: 0.10.13 + satori-html: + specifier: ^0.3.2 + version: 0.3.2 + sharp: + specifier: ^0.33.2 + version: 0.33.2 slugify: specifier: ^1.6.6 version: 1.6.6 @@ -1222,6 +1243,132 @@ packages: - immer dev: false + /@resvg/resvg-js-android-arm-eabi@2.6.0: + resolution: {integrity: sha512-lJnZ/2P5aMocrFMW7HWhVne5gH82I8xH6zsfH75MYr4+/JOaVcGCTEQ06XFohGMdYRP3v05SSPLPvTM/RHjxfA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-android-arm64@2.6.0: + resolution: {integrity: sha512-N527f529bjMwYWShZYfBD60dXA4Fux+D695QsHQ93BDYZSHUoOh1CUGUyICevnTxs7VgEl98XpArmUWBZQVMfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-darwin-arm64@2.6.0: + resolution: {integrity: sha512-MabUKLVayEwlPo0mIqAmMt+qESN8LltCvv5+GLgVga1avpUrkxj/fkU1TKm8kQegutUjbP/B0QuMuUr0uhF8ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-darwin-x64@2.6.0: + resolution: {integrity: sha512-zrFetdnSw/suXjmyxSjfDV7i61hahv6DDG6kM7BYN2yJ3Es5+BZtqYZTcIWogPJedYKmzN1YTMWGd/3f0ubFiA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-arm-gnueabihf@2.6.0: + resolution: {integrity: sha512-sH4gxXt7v7dGwjGyzLwn7SFGvwZG6DQqLaZ11MmzbCwd9Zosy1TnmrMJfn6TJ7RHezmQMgBPi18bl55FZ1AT4A==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-arm64-gnu@2.6.0: + resolution: {integrity: sha512-fCyMncqCJtrlANADIduYF4IfnWQ295UKib7DAxFXQhBsM9PLDTpizr0qemZcCNadcwSVHnAIzL4tliZhCM8P6A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-arm64-musl@2.6.0: + resolution: {integrity: sha512-ouLjTgBQHQyxLht4FdMPTvuY8xzJigM9EM2Tlu0llWkN1mKyTQrvYWi6TA6XnKdzDJHy7ZLpWpjZi7F5+Pg+Vg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-x64-gnu@2.6.0: + resolution: {integrity: sha512-n3zC8DWsvxC1AwxpKFclIPapDFibs5XdIRoV/mcIlxlh0vseW1F49b97F33BtJQRmlntsqqN6GMMqx8byB7B+Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-linux-x64-musl@2.6.0: + resolution: {integrity: sha512-n4tasK1HOlAxdTEROgYA1aCfsEKk0UOFDNd/AQTTZlTmCbHKXPq+O8npaaKlwXquxlVK8vrkcWbksbiGqbCAcw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-win32-arm64-msvc@2.6.0: + resolution: {integrity: sha512-X2+EoBJFwDI5LDVb51Sk7ldnVLitMGr9WwU/i21i3fAeAXZb3hM16k67DeTy16OYkT2dk/RfU1tP1wG+rWbz2Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-win32-ia32-msvc@2.6.0: + resolution: {integrity: sha512-L7oevWjQoUgK5W1fCKn0euSVemhDXVhrjtwqpc7MwBKKimYeiOshO1Li1pa8bBt5PESahenhWgdB6lav9O0fEg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js-win32-x64-msvc@2.6.0: + resolution: {integrity: sha512-8lJlghb+Unki5AyKgsnFbRJwkEj9r1NpwyuBG8yEJiG1W9eEGl03R3I7bsVa3haof/3J1NlWf0rzSa1G++A2iw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@resvg/resvg-js@2.6.0: + resolution: {integrity: sha512-Tf3YpbBKcQn991KKcw/vg7vZf98v01seSv6CVxZBbRkL/xyjnoYB6KgrFL6zskT1A4dWC/vg77KyNOW+ePaNlA==} + engines: {node: '>= 10'} + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.0 + '@resvg/resvg-js-android-arm64': 2.6.0 + '@resvg/resvg-js-darwin-arm64': 2.6.0 + '@resvg/resvg-js-darwin-x64': 2.6.0 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.0 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.0 + '@resvg/resvg-js-linux-arm64-musl': 2.6.0 + '@resvg/resvg-js-linux-x64-gnu': 2.6.0 + '@resvg/resvg-js-linux-x64-musl': 2.6.0 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.0 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.0 + '@resvg/resvg-js-win32-x64-msvc': 2.6.0 + dev: false + /@rollup/rollup-android-arm-eabi@4.9.6: resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} cpu: [arm] @@ -1326,6 +1473,15 @@ packages: dev: false optional: true + /@shuding/opentype.js@1.4.0-beta.0: + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + dev: false + /@sigstore/bundle@1.1.0: resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2047,6 +2203,11 @@ packages: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} dev: false + /base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false @@ -2223,6 +2384,10 @@ packages: engines: {node: '>=14.16'} dev: false + /camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + dev: false + /caniuse-lite@1.0.30001579: resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} dev: false @@ -2364,6 +2529,7 @@ packages: /color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + requiresBuild: true dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 @@ -2477,6 +2643,19 @@ packages: type-fest: 1.4.0 dev: false + /css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + dev: false + + /css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + dev: false + + /css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + dev: false + /css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} dependencies: @@ -2487,6 +2666,14 @@ packages: nth-check: 2.1.1 dev: false + /css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + dev: false + /css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -2854,6 +3041,10 @@ packages: engines: {node: '>=12'} dev: false + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -2946,6 +3137,10 @@ packages: dependencies: reusify: 1.0.4 + /fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + dev: false + /filename-reserved-regex@2.0.0: resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} engines: {node: '>=4'} @@ -3402,6 +3597,11 @@ packages: hasBin: true dev: false + /hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + dev: false + /hosted-git-info@5.2.1: resolution: {integrity: sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3416,6 +3616,10 @@ packages: lru-cache: 7.18.3 dev: false + /htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + dev: false + /html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} dev: false @@ -3506,6 +3710,14 @@ packages: engines: {node: '>= 4'} dev: false + /image-size@1.1.1: + resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} + engines: {node: '>=16.x'} + hasBin: true + dependencies: + queue: 6.0.2 + dev: false + /import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -3563,6 +3775,7 @@ packages: /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + requiresBuild: true dev: false /is-binary-path@2.1.0: @@ -3916,6 +4129,13 @@ packages: resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} engines: {node: '>=14'} + /linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5116,6 +5336,10 @@ packages: - supports-color dev: false + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: false + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -5123,6 +5347,13 @@ packages: tslib: 2.6.2 dev: false + /parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + dev: false + /parse-github-url@1.0.2: resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==} engines: {node: '>=0.10.0'} @@ -5505,6 +5736,12 @@ packages: dev: false optional: true + /queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + dependencies: + inherits: 2.0.4 + dev: false + /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -5886,6 +6123,28 @@ packages: suf-log: 2.5.3 dev: true + /satori-html@0.3.2: + resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==} + dependencies: + ultrahtml: 1.5.2 + dev: false + + /satori@0.10.13: + resolution: {integrity: sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ==} + engines: {node: '>=16'} + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-to-react-native: 3.2.0 + emoji-regex: 10.3.0 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + dev: false + /sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} dev: false @@ -6041,6 +6300,7 @@ packages: /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + requiresBuild: true dependencies: is-arrayish: 0.3.2 dev: false @@ -6211,6 +6471,10 @@ packages: strip-ansi: 7.1.0 dev: false + /string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + dev: false + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -6427,6 +6691,10 @@ packages: dependencies: any-promise: 1.3.0 + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: false + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -6529,6 +6797,13 @@ packages: resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} dev: false + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: false + /unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} dependencies: @@ -6945,6 +7220,10 @@ packages: engines: {node: '>=12.20'} dev: false + /yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + dev: false + /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false diff --git a/public/fonts/BalsamiqSans-Regular.ttf b/public/fonts/BalsamiqSans-Regular.ttf new file mode 100644 index 000000000..4fc9f82e9 Binary files /dev/null and b/public/fonts/BalsamiqSans-Regular.ttf differ diff --git a/public/images/graph.svg b/public/images/graph.svg new file mode 100644 index 000000000..51f61aa1c --- /dev/null +++ b/public/images/graph.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/og-images/best-practices/api-security.png b/public/og-images/best-practices/api-security.png new file mode 100644 index 000000000..2b223533d Binary files /dev/null and b/public/og-images/best-practices/api-security.png differ diff --git a/public/og-images/best-practices/aws.png b/public/og-images/best-practices/aws.png new file mode 100644 index 000000000..0d736a471 Binary files /dev/null and b/public/og-images/best-practices/aws.png differ diff --git a/public/og-images/best-practices/code-review.png b/public/og-images/best-practices/code-review.png new file mode 100644 index 000000000..2fc5b01b2 Binary files /dev/null and b/public/og-images/best-practices/code-review.png differ diff --git a/public/og-images/best-practices/frontend-performance.png b/public/og-images/best-practices/frontend-performance.png new file mode 100644 index 000000000..2b5f9be3f Binary files /dev/null and b/public/og-images/best-practices/frontend-performance.png differ diff --git a/public/og-images/guides/asymptotic-notation.png b/public/og-images/guides/asymptotic-notation.png new file mode 100644 index 000000000..921123291 Binary files /dev/null and b/public/og-images/guides/asymptotic-notation.png differ diff --git a/public/og-images/guides/avoid-render-blocking-javascript-with-async-defer.png b/public/og-images/guides/avoid-render-blocking-javascript-with-async-defer.png new file mode 100644 index 000000000..1ea6f9b15 Binary files /dev/null and b/public/og-images/guides/avoid-render-blocking-javascript-with-async-defer.png differ diff --git a/public/og-images/guides/backend-developer-skills.png b/public/og-images/guides/backend-developer-skills.png new file mode 100644 index 000000000..492feba01 Binary files /dev/null and b/public/og-images/guides/backend-developer-skills.png differ diff --git a/public/og-images/guides/backend-languages.png b/public/og-images/guides/backend-languages.png new file mode 100644 index 000000000..c3f383a65 Binary files /dev/null and b/public/og-images/guides/backend-languages.png differ diff --git a/public/og-images/guides/basic-authentication.png b/public/og-images/guides/basic-authentication.png new file mode 100644 index 000000000..6ba219322 Binary files /dev/null and b/public/og-images/guides/basic-authentication.png differ diff --git a/public/og-images/guides/basics-of-authentication.png b/public/og-images/guides/basics-of-authentication.png new file mode 100644 index 000000000..dd9bdcebf Binary files /dev/null and b/public/og-images/guides/basics-of-authentication.png differ diff --git a/public/og-images/guides/big-o-notation.png b/public/og-images/guides/big-o-notation.png new file mode 100644 index 000000000..c55d03c30 Binary files /dev/null and b/public/og-images/guides/big-o-notation.png differ diff --git a/public/og-images/guides/character-encodings.png b/public/og-images/guides/character-encodings.png new file mode 100644 index 000000000..368d80239 Binary files /dev/null and b/public/og-images/guides/character-encodings.png differ diff --git a/public/og-images/guides/ci-cd.png b/public/og-images/guides/ci-cd.png new file mode 100644 index 000000000..32163c495 Binary files /dev/null and b/public/og-images/guides/ci-cd.png differ diff --git a/public/og-images/guides/consistency-patterns-in-distributed-systems.png b/public/og-images/guides/consistency-patterns-in-distributed-systems.png new file mode 100644 index 000000000..ca233251e Binary files /dev/null and b/public/og-images/guides/consistency-patterns-in-distributed-systems.png differ diff --git a/public/og-images/guides/design-patterns-for-humans.png b/public/og-images/guides/design-patterns-for-humans.png new file mode 100644 index 000000000..54ef033cd Binary files /dev/null and b/public/og-images/guides/design-patterns-for-humans.png differ diff --git a/public/og-images/guides/dhcp-in-one-picture.png b/public/og-images/guides/dhcp-in-one-picture.png new file mode 100644 index 000000000..acf29d7a0 Binary files /dev/null and b/public/og-images/guides/dhcp-in-one-picture.png differ diff --git a/public/og-images/guides/dns-in-one-picture.png b/public/og-images/guides/dns-in-one-picture.png new file mode 100644 index 000000000..2059486f4 Binary files /dev/null and b/public/og-images/guides/dns-in-one-picture.png differ diff --git a/public/og-images/guides/free-resources-to-learn-llms.png b/public/og-images/guides/free-resources-to-learn-llms.png new file mode 100644 index 000000000..78c6cebfe Binary files /dev/null and b/public/og-images/guides/free-resources-to-learn-llms.png differ diff --git a/public/og-images/guides/history-of-javascript.png b/public/og-images/guides/history-of-javascript.png new file mode 100644 index 000000000..77d462465 Binary files /dev/null and b/public/og-images/guides/history-of-javascript.png differ diff --git a/public/og-images/guides/how-to-setup-a-jump-server.png b/public/og-images/guides/how-to-setup-a-jump-server.png new file mode 100644 index 000000000..b5f0dc813 Binary files /dev/null and b/public/og-images/guides/how-to-setup-a-jump-server.png differ diff --git a/public/og-images/guides/http-basic-authentication.png b/public/og-images/guides/http-basic-authentication.png new file mode 100644 index 000000000..c9046bd75 Binary files /dev/null and b/public/og-images/guides/http-basic-authentication.png differ diff --git a/public/og-images/guides/http-caching.png b/public/og-images/guides/http-caching.png new file mode 100644 index 000000000..6ef24a29d Binary files /dev/null and b/public/og-images/guides/http-caching.png differ diff --git a/public/og-images/guides/introduction-to-llms.png b/public/og-images/guides/introduction-to-llms.png new file mode 100644 index 000000000..36f51062d Binary files /dev/null and b/public/og-images/guides/introduction-to-llms.png differ diff --git a/public/og-images/guides/journey-to-http2.png b/public/og-images/guides/journey-to-http2.png new file mode 100644 index 000000000..7e40ed967 Binary files /dev/null and b/public/og-images/guides/journey-to-http2.png differ diff --git a/public/og-images/guides/jwt-authentication.png b/public/og-images/guides/jwt-authentication.png new file mode 100644 index 000000000..25253dcc8 Binary files /dev/null and b/public/og-images/guides/jwt-authentication.png differ diff --git a/public/og-images/guides/levels-of-seniority.png b/public/og-images/guides/levels-of-seniority.png new file mode 100644 index 000000000..ea9de6478 Binary files /dev/null and b/public/og-images/guides/levels-of-seniority.png differ diff --git a/public/og-images/guides/oauth.png b/public/og-images/guides/oauth.png new file mode 100644 index 000000000..858fa198d Binary files /dev/null and b/public/og-images/guides/oauth.png differ diff --git a/public/og-images/guides/proxy-servers.png b/public/og-images/guides/proxy-servers.png new file mode 100644 index 000000000..4575d08e0 Binary files /dev/null and b/public/og-images/guides/proxy-servers.png differ diff --git a/public/og-images/guides/random-numbers.png b/public/og-images/guides/random-numbers.png new file mode 100644 index 000000000..c34710534 Binary files /dev/null and b/public/og-images/guides/random-numbers.png differ diff --git a/public/og-images/guides/scaling-databases.png b/public/og-images/guides/scaling-databases.png new file mode 100644 index 000000000..0e251e355 Binary files /dev/null and b/public/og-images/guides/scaling-databases.png differ diff --git a/public/og-images/guides/session-authentication.png b/public/og-images/guides/session-authentication.png new file mode 100644 index 000000000..5f20cf034 Binary files /dev/null and b/public/og-images/guides/session-authentication.png differ diff --git a/public/og-images/guides/session-based-authentication.png b/public/og-images/guides/session-based-authentication.png new file mode 100644 index 000000000..f27349ea9 Binary files /dev/null and b/public/og-images/guides/session-based-authentication.png differ diff --git a/public/og-images/guides/setup-and-auto-renew-ssl-certificates.png b/public/og-images/guides/setup-and-auto-renew-ssl-certificates.png new file mode 100644 index 000000000..764565dee Binary files /dev/null and b/public/og-images/guides/setup-and-auto-renew-ssl-certificates.png differ diff --git a/public/og-images/guides/single-command-database-setup.png b/public/og-images/guides/single-command-database-setup.png new file mode 100644 index 000000000..f59c1461c Binary files /dev/null and b/public/og-images/guides/single-command-database-setup.png differ diff --git a/public/og-images/guides/ssl-tls-https-ssh.png b/public/og-images/guides/ssl-tls-https-ssh.png new file mode 100644 index 000000000..23c643a98 Binary files /dev/null and b/public/og-images/guides/ssl-tls-https-ssh.png differ diff --git a/public/og-images/guides/sso.png b/public/og-images/guides/sso.png new file mode 100644 index 000000000..964611c47 Binary files /dev/null and b/public/og-images/guides/sso.png differ diff --git a/public/og-images/guides/token-authentication.png b/public/og-images/guides/token-authentication.png new file mode 100644 index 000000000..df474f238 Binary files /dev/null and b/public/og-images/guides/token-authentication.png differ diff --git a/public/og-images/guides/torrent-client.png b/public/og-images/guides/torrent-client.png new file mode 100644 index 000000000..5402dfcf0 Binary files /dev/null and b/public/og-images/guides/torrent-client.png differ diff --git a/public/og-images/guides/unfamiliar-codebase.png b/public/og-images/guides/unfamiliar-codebase.png new file mode 100644 index 000000000..9b30849a6 Binary files /dev/null and b/public/og-images/guides/unfamiliar-codebase.png differ diff --git a/public/og-images/guides/what-are-web-vitals.png b/public/og-images/guides/what-are-web-vitals.png new file mode 100644 index 000000000..ffaa0699c Binary files /dev/null and b/public/og-images/guides/what-are-web-vitals.png differ diff --git a/public/og-images/guides/what-is-internet.png b/public/og-images/guides/what-is-internet.png new file mode 100644 index 000000000..ebc49266d Binary files /dev/null and b/public/og-images/guides/what-is-internet.png differ diff --git a/public/og-images/guides/what-is-sli-slo-sla.png b/public/og-images/guides/what-is-sli-slo-sla.png new file mode 100644 index 000000000..1cd1cbd01 Binary files /dev/null and b/public/og-images/guides/what-is-sli-slo-sla.png differ diff --git a/public/og-images/guides/why-build-it-and-they-will-come-wont-work-anymore.png b/public/og-images/guides/why-build-it-and-they-will-come-wont-work-anymore.png new file mode 100644 index 000000000..07d31f813 Binary files /dev/null and b/public/og-images/guides/why-build-it-and-they-will-come-wont-work-anymore.png differ diff --git a/public/og-images/roadmaps/ai-data-scientist.png b/public/og-images/roadmaps/ai-data-scientist.png new file mode 100644 index 000000000..8f92bd7e4 Binary files /dev/null and b/public/og-images/roadmaps/ai-data-scientist.png differ diff --git a/public/og-images/roadmaps/android.png b/public/og-images/roadmaps/android.png new file mode 100644 index 000000000..22185d37b Binary files /dev/null and b/public/og-images/roadmaps/android.png differ diff --git a/public/og-images/roadmaps/angular.png b/public/og-images/roadmaps/angular.png new file mode 100644 index 000000000..93ed3436f Binary files /dev/null and b/public/og-images/roadmaps/angular.png differ diff --git a/public/og-images/roadmaps/aspnet-core.png b/public/og-images/roadmaps/aspnet-core.png new file mode 100644 index 000000000..e76626536 Binary files /dev/null and b/public/og-images/roadmaps/aspnet-core.png differ diff --git a/public/og-images/roadmaps/aws.png b/public/og-images/roadmaps/aws.png new file mode 100644 index 000000000..d483cc8a6 Binary files /dev/null and b/public/og-images/roadmaps/aws.png differ diff --git a/public/og-images/roadmaps/backend.png b/public/og-images/roadmaps/backend.png new file mode 100644 index 000000000..adb0f1345 Binary files /dev/null and b/public/og-images/roadmaps/backend.png differ diff --git a/public/og-images/roadmaps/blockchain.png b/public/og-images/roadmaps/blockchain.png new file mode 100644 index 000000000..d99d16985 Binary files /dev/null and b/public/og-images/roadmaps/blockchain.png differ diff --git a/public/og-images/roadmaps/code-review.png b/public/og-images/roadmaps/code-review.png new file mode 100644 index 000000000..055390d47 Binary files /dev/null and b/public/og-images/roadmaps/code-review.png differ diff --git a/public/og-images/roadmaps/computer-science.png b/public/og-images/roadmaps/computer-science.png new file mode 100644 index 000000000..50bb98a5c Binary files /dev/null and b/public/og-images/roadmaps/computer-science.png differ diff --git a/public/og-images/roadmaps/cpp.png b/public/og-images/roadmaps/cpp.png new file mode 100644 index 000000000..a0622b049 Binary files /dev/null and b/public/og-images/roadmaps/cpp.png differ diff --git a/public/og-images/roadmaps/cyber-security.png b/public/og-images/roadmaps/cyber-security.png new file mode 100644 index 000000000..4417371c7 Binary files /dev/null and b/public/og-images/roadmaps/cyber-security.png differ diff --git a/public/og-images/roadmaps/datastructures-and-algorithms.png b/public/og-images/roadmaps/datastructures-and-algorithms.png new file mode 100644 index 000000000..8a349c525 Binary files /dev/null and b/public/og-images/roadmaps/datastructures-and-algorithms.png differ diff --git a/public/og-images/roadmaps/design-system.png b/public/og-images/roadmaps/design-system.png new file mode 100644 index 000000000..db30cbade Binary files /dev/null and b/public/og-images/roadmaps/design-system.png differ diff --git a/public/og-images/roadmaps/devops.png b/public/og-images/roadmaps/devops.png new file mode 100644 index 000000000..bacedd8ae Binary files /dev/null and b/public/og-images/roadmaps/devops.png differ diff --git a/public/og-images/roadmaps/docker.png b/public/og-images/roadmaps/docker.png new file mode 100644 index 000000000..6ddc7c738 Binary files /dev/null and b/public/og-images/roadmaps/docker.png differ diff --git a/public/og-images/roadmaps/flutter.png b/public/og-images/roadmaps/flutter.png new file mode 100644 index 000000000..cb525de86 Binary files /dev/null and b/public/og-images/roadmaps/flutter.png differ diff --git a/public/og-images/roadmaps/frontend.png b/public/og-images/roadmaps/frontend.png new file mode 100644 index 000000000..5607bab4c Binary files /dev/null and b/public/og-images/roadmaps/frontend.png differ diff --git a/public/og-images/roadmaps/full-stack.png b/public/og-images/roadmaps/full-stack.png new file mode 100644 index 000000000..c0110efb2 Binary files /dev/null and b/public/og-images/roadmaps/full-stack.png differ diff --git a/public/og-images/roadmaps/game-developer.png b/public/og-images/roadmaps/game-developer.png new file mode 100644 index 000000000..9023ca835 Binary files /dev/null and b/public/og-images/roadmaps/game-developer.png differ diff --git a/public/og-images/roadmaps/golang.png b/public/og-images/roadmaps/golang.png new file mode 100644 index 000000000..cc41702b0 Binary files /dev/null and b/public/og-images/roadmaps/golang.png differ diff --git a/public/og-images/roadmaps/graphql.png b/public/og-images/roadmaps/graphql.png new file mode 100644 index 000000000..df845bcad Binary files /dev/null and b/public/og-images/roadmaps/graphql.png differ diff --git a/public/og-images/roadmaps/java.png b/public/og-images/roadmaps/java.png new file mode 100644 index 000000000..21c0f8305 Binary files /dev/null and b/public/og-images/roadmaps/java.png differ diff --git a/public/og-images/roadmaps/javascript.png b/public/og-images/roadmaps/javascript.png new file mode 100644 index 000000000..6f51c790e Binary files /dev/null and b/public/og-images/roadmaps/javascript.png differ diff --git a/public/og-images/roadmaps/kubernetes.png b/public/og-images/roadmaps/kubernetes.png new file mode 100644 index 000000000..6dc011149 Binary files /dev/null and b/public/og-images/roadmaps/kubernetes.png differ diff --git a/public/og-images/roadmaps/mlops.png b/public/og-images/roadmaps/mlops.png new file mode 100644 index 000000000..50f78c154 Binary files /dev/null and b/public/og-images/roadmaps/mlops.png differ diff --git a/public/og-images/roadmaps/mongodb.png b/public/og-images/roadmaps/mongodb.png new file mode 100644 index 000000000..c4b152ddd Binary files /dev/null and b/public/og-images/roadmaps/mongodb.png differ diff --git a/public/og-images/roadmaps/nodejs.png b/public/og-images/roadmaps/nodejs.png new file mode 100644 index 000000000..bd05279b5 Binary files /dev/null and b/public/og-images/roadmaps/nodejs.png differ diff --git a/public/og-images/roadmaps/postgresql-dba.png b/public/og-images/roadmaps/postgresql-dba.png new file mode 100644 index 000000000..7fd7cfa86 Binary files /dev/null and b/public/og-images/roadmaps/postgresql-dba.png differ diff --git a/public/og-images/roadmaps/prompt-engineering.png b/public/og-images/roadmaps/prompt-engineering.png new file mode 100644 index 000000000..c308dfd40 Binary files /dev/null and b/public/og-images/roadmaps/prompt-engineering.png differ diff --git a/public/og-images/roadmaps/python.png b/public/og-images/roadmaps/python.png new file mode 100644 index 000000000..35687f082 Binary files /dev/null and b/public/og-images/roadmaps/python.png differ diff --git a/public/og-images/roadmaps/qa.png b/public/og-images/roadmaps/qa.png new file mode 100644 index 000000000..283ac7a8f Binary files /dev/null and b/public/og-images/roadmaps/qa.png differ diff --git a/public/og-images/roadmaps/react-native.png b/public/og-images/roadmaps/react-native.png new file mode 100644 index 000000000..5083e915f Binary files /dev/null and b/public/og-images/roadmaps/react-native.png differ diff --git a/public/og-images/roadmaps/react.png b/public/og-images/roadmaps/react.png new file mode 100644 index 000000000..f3f6792ff Binary files /dev/null and b/public/og-images/roadmaps/react.png differ diff --git a/public/og-images/roadmaps/rust.png b/public/og-images/roadmaps/rust.png new file mode 100644 index 000000000..9cb0233aa Binary files /dev/null and b/public/og-images/roadmaps/rust.png differ diff --git a/public/og-images/roadmaps/server-side-game-developer.png b/public/og-images/roadmaps/server-side-game-developer.png new file mode 100644 index 000000000..89759d0a0 Binary files /dev/null and b/public/og-images/roadmaps/server-side-game-developer.png differ diff --git a/public/og-images/roadmaps/software-architect.png b/public/og-images/roadmaps/software-architect.png new file mode 100644 index 000000000..41adfc831 Binary files /dev/null and b/public/og-images/roadmaps/software-architect.png differ diff --git a/public/og-images/roadmaps/software-design-architecture.png b/public/og-images/roadmaps/software-design-architecture.png new file mode 100644 index 000000000..3b9d260ef Binary files /dev/null and b/public/og-images/roadmaps/software-design-architecture.png differ diff --git a/public/og-images/roadmaps/spring-boot.png b/public/og-images/roadmaps/spring-boot.png new file mode 100644 index 000000000..ca550d6b7 Binary files /dev/null and b/public/og-images/roadmaps/spring-boot.png differ diff --git a/public/og-images/roadmaps/sql.png b/public/og-images/roadmaps/sql.png new file mode 100644 index 000000000..7a30c975d Binary files /dev/null and b/public/og-images/roadmaps/sql.png differ diff --git a/public/og-images/roadmaps/system-design.png b/public/og-images/roadmaps/system-design.png new file mode 100644 index 000000000..1dfd75e7d Binary files /dev/null and b/public/og-images/roadmaps/system-design.png differ diff --git a/public/og-images/roadmaps/technical-writer.png b/public/og-images/roadmaps/technical-writer.png new file mode 100644 index 000000000..a0250aafb Binary files /dev/null and b/public/og-images/roadmaps/technical-writer.png differ diff --git a/public/og-images/roadmaps/typescript.png b/public/og-images/roadmaps/typescript.png new file mode 100644 index 000000000..df2128886 Binary files /dev/null and b/public/og-images/roadmaps/typescript.png differ diff --git a/public/og-images/roadmaps/ux-design.png b/public/og-images/roadmaps/ux-design.png new file mode 100644 index 000000000..590445acf Binary files /dev/null and b/public/og-images/roadmaps/ux-design.png differ diff --git a/public/og-images/roadmaps/vue.png b/public/og-images/roadmaps/vue.png new file mode 100644 index 000000000..05c7e0e16 Binary files /dev/null and b/public/og-images/roadmaps/vue.png differ diff --git a/scripts/generate-og-images.mjs b/scripts/generate-og-images.mjs new file mode 100644 index 000000000..1c1357f9a --- /dev/null +++ b/scripts/generate-og-images.mjs @@ -0,0 +1,554 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; +import matter from 'gray-matter'; +import { html } from 'satori-html'; +import satori from 'satori'; +import sharp from 'sharp'; +import imageSize from 'image-size'; +import { Resvg } from '@resvg/resvg-js'; + +const ALL_ROADMAP_DIR = path.join(process.cwd(), '/src/data/roadmaps'); +const ALL_BEST_PRACTICE_DIR = path.join( + process.cwd(), + '/src/data/best-practices', +); +const ALL_GUIDE_DIR = path.join(process.cwd(), '/src/data/guides'); +const ALl_AUTHOR_DIR = path.join(process.cwd(), '/src/data/authors'); +const ALL_ROADMAP_IMAGE_DIR = path.join(process.cwd(), '/public/roadmaps'); +const ALL_BEST_PRACTICE_IMAGE_DIR = path.join( + process.cwd(), + '/public/best-practices', +); +const ALL_AUTHOR_IMAGE_DIR = path.join(process.cwd(), '/public'); + +const alreadyGeneratedImages = await fs.readdir( + path.join(process.cwd(), '/public/og-images'), + { + recursive: true, + }, +); + +async function getAllRoadmaps() { + const allRoadmapDirNames = await fs.readdir(ALL_ROADMAP_DIR); + + const allRoadmapFrontmatter = await Promise.all( + allRoadmapDirNames.map(async (roadmapDirName) => { + const roadmapDirPath = path.join( + ALL_ROADMAP_DIR, + roadmapDirName, + `${roadmapDirName}.md`, + ); + + const markdown = await fs.readFile(roadmapDirPath, 'utf8'); + const { data } = matter(markdown); + + return { + id: roadmapDirName, + title: data?.briefTitle, + description: data?.briefDescription, + }; + }), + ); + + return allRoadmapFrontmatter; +} + +async function getAllBestPractices() { + const allBestPracticeDirNames = await fs.readdir(ALL_BEST_PRACTICE_DIR); + + const allBestPracticeFrontmatter = await Promise.all( + allBestPracticeDirNames.map(async (bestPracticeDirName) => { + const bestPracticeDirPath = path.join( + ALL_BEST_PRACTICE_DIR, + bestPracticeDirName, + `${bestPracticeDirName}.md`, + ); + + const markdown = await fs.readFile(bestPracticeDirPath, 'utf8'); + const { data } = matter(markdown); + + return { + id: bestPracticeDirName, + title: data?.briefTitle, + description: data?.briefDescription, + }; + }), + ); + + return allBestPracticeFrontmatter; +} + +async function getAllGuides() { + const allGuideDirNames = await fs.readdir(ALL_GUIDE_DIR); + + const allGuideFrontmatter = await Promise.all( + allGuideDirNames.map(async (guideDirName) => { + const guideDirPath = path.join(ALL_GUIDE_DIR, guideDirName); + + const markdown = await fs.readFile(guideDirPath, 'utf8'); + const { data } = matter(markdown); + + return { + id: guideDirName?.replace('.md', ''), + title: data?.title, + description: data?.description, + authorId: data?.authorId, + }; + }), + ); + + return allGuideFrontmatter; +} + +async function getAllAuthors() { + const allAuthorDirNames = await fs.readdir(ALl_AUTHOR_DIR); + + const allAuthorFrontmatter = await Promise.all( + allAuthorDirNames.map(async (authorDirName) => { + const authorDirPath = path.join(ALl_AUTHOR_DIR, authorDirName); + + const markdown = await fs.readFile(authorDirPath, 'utf8'); + const { data } = matter(markdown); + + return { + id: authorDirName?.replace('.md', ''), + name: data?.name, + imageUrl: data?.imageUrl, + }; + }), + ); + + return allAuthorFrontmatter; +} + +async function getAllRoadmapImageIds() { + const allRoadmapImageDirNames = await fs.readdir(ALL_ROADMAP_IMAGE_DIR); + + return allRoadmapImageDirNames?.reduce((acc, image) => { + acc[image.replace(/(\.[^.]*)$/, '')] = image; + return acc; + }, {}); +} + +async function getAllBestPracticeImageIds() { + const allBestPracticeImageDirNames = await fs.readdir( + ALL_BEST_PRACTICE_IMAGE_DIR, + ); + + return allBestPracticeImageDirNames?.reduce((acc, image) => { + acc[image.replace(/(\.[^.]*)$/, '')] = image; + return acc; + }, {}); +} + +async function generateResourceOpenGraph() { + const allRoadmaps = (await getAllRoadmaps()).filter( + (roadmap) => !alreadyGeneratedImages.includes(`roadmaps/${roadmap.id}.png`), + ); + const allBestPractices = (await getAllBestPractices()).filter( + (bestPractice) => + !alreadyGeneratedImages.includes(`best-practices/${bestPractice.id}.png`), + ); + const allRoadmapImageIds = await getAllRoadmapImageIds(); + const allBestPracticeImageIds = await getAllBestPracticeImageIds(); + + const resources = []; + allRoadmaps.forEach((roadmap) => { + const hasImage = allRoadmapImageIds?.[roadmap.id]; + resources.push({ + type: 'roadmaps', + id: roadmap.id, + title: roadmap.title, + description: roadmap.description, + image: hasImage + ? path.join(ALL_ROADMAP_IMAGE_DIR, allRoadmapImageIds[roadmap.id]) + : null, + }); + }); + + allBestPractices.forEach((bestPractice) => { + const hasImage = allBestPracticeImageIds?.[bestPractice.id]; + resources.push({ + type: 'best-practices', + id: bestPractice.id, + title: bestPractice.title, + description: bestPractice.description, + image: hasImage + ? path.join( + ALL_BEST_PRACTICE_IMAGE_DIR, + allBestPracticeImageIds[bestPractice.id], + ) + : null, + }); + }); + + for (const resource of resources) { + if (!resource.image) { + let template = getRoadmapDefaultTemplate(resource); + if ( + hasSpecialCharacters(resource.title) || + hasSpecialCharacters(resource.description) + ) { + // For some reason special characters are not being rendered properly + // https://github.com/natemoo-re/satori-html/issues/20 + // So we need to unescape the html + template = JSON.parse(unescapeHtml(JSON.stringify(template))); + } + await generateOpenGraph( + template, + resource.type, + resource.id + '.png', + 'resvg', + ); + } else { + const image = await fs.readFile(resource.image); + const dimensions = imageSize(image); + + const widthRatio = 1200 / dimensions.width; + let width = dimensions.width * widthRatio * 0.85; + let height = dimensions.height * widthRatio * 0.85; + + let template = getRoadmapImageTemplate({ + ...resource, + image: `data:image/${dimensions.type};base64,${image.toString('base64')}`, + width, + height, + }); + + if ( + hasSpecialCharacters(resource.title) || + hasSpecialCharacters(resource.description) + ) { + // For some reason special characters are not being rendered properly + // https://github.com/natemoo-re/satori-html/issues/20 + // So we need to unescape the html + template = JSON.parse(unescapeHtml(JSON.stringify(template))); + } + + await generateOpenGraph(template, resource.type, resource.id + '.png'); + } + } +} + +async function generateGuideOpenGraph() { + const allGuides = (await getAllGuides()).filter( + (guide) => !alreadyGeneratedImages.includes(`guides/${guide.id}.png`), + ); + const allAuthors = await getAllAuthors(); + + for (const guide of allGuides) { + const author = allAuthors.find((author) => author.id === guide.authorId); + const image = + author?.imageUrl || 'https://roadmap.sh/images/default-avatar.png'; + const isExternalImage = image?.startsWith('http'); + let authorImageExtention = ''; + let authorAvatar; + if (!isExternalImage) { + authorAvatar = await fs.readFile(path.join(ALL_AUTHOR_IMAGE_DIR, image)); + authorImageExtention = image?.split('.')[1]; + } + + const template = getGuideTemplate({ + ...guide, + authorName: author.name, + authorAvatar: isExternalImage + ? image + : `data:image/${authorImageExtention};base64,${authorAvatar.toString('base64')}`, + }); + if ( + hasSpecialCharacters(guide.title) || + hasSpecialCharacters(guide.description) + ) { + // For some reason special characters are not being rendered properly + // https://github.com/natemoo-re/satori-html/issues/20 + // So we need to unescape the html + template = JSON.parse(unescapeHtml(JSON.stringify(template))); + } + await generateOpenGraph(template, 'guides', guide.id + '.png'); + } +} + +async function generateOpenGraph( + htmlString, + type, + fileName, + renderer = 'sharp', +) { + console.log('Started 🚀', `${type}/${fileName}`); + const svg = await satori(htmlString, { + width: 1200, + height: 630, + fonts: [ + { + name: 'balsamiq', + data: await fs.readFile( + path.join(process.cwd(), '/public/fonts/BalsamiqSans-Regular.ttf'), + ), + weight: 400, + style: 'normal', + }, + ], + }); + + await fs.mkdir(path.join(process.cwd(), '/public/og-images/' + type), { + recursive: true, + }); + // It will be used to generate the default image + // for some reasone sharp is not working with this + // FIXME: Investigate why sharp is not working with this + if (renderer === 'resvg') { + const resvg = new Resvg(svg, { + fitTo: { + mode: 'width', + value: 2500, + }, + }); + const pngData = resvg.render(); + const pngBuffer = pngData.asPng(); + await fs.writeFile( + path.join(process.cwd(), '/public/og-images/' + `${type}/${fileName}`), + pngBuffer, + ); + } else { + await sharp(Buffer.from(svg), { density: 150 }) + .png() + .toFile( + path.join(process.cwd(), '/public/og-images/' + `${type}/${fileName}`), + ); + } + + console.log('Completed ✅', `${type}/${fileName}`); +} + +await generateResourceOpenGraph(); +await generateGuideOpenGraph(); + +function getRoadmapDefaultTemplate({ title, description }) { + return html`
+
+
+
+
+
+
+ +
+
+
+
${title}
+
+ ${description} +
+
+ +
+
+
+ + + +
+
+ 6th most starred GitHub project +
+
+
+
+ + + + + + + + + +
+
+ Created and maintained by community +
+
+
+
+ + + + +
+
Up-to-date roadmap
+
+
+
+
+
`; +} + +function getRoadmapImageTemplate({ title, description, image, height, width }) { + return html`
+
+ +
+
+
+ ${title?.replace('&', `{"&"}`)} +
+
+ ${description} +
+
+
+ + +
`; +} + +function getGuideTemplate({ title, description, authorName, authorAvatar }) { + return html`
+
+
+
+
+
+
+ +
+
+
+
+ +
+ ${authorName} +
+
+
${title}
+
+ ${description} +
+
+
+
+
`; +} + +function unescapeHtml(html) { + return html + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'"); +} + +function hasSpecialCharacters(str) { + return /[&<>"]/.test(str); +} diff --git a/src/lib/author.ts b/src/lib/author.ts index ac6606fe4..bc97ce690 100644 --- a/src/lib/author.ts +++ b/src/lib/author.ts @@ -38,7 +38,6 @@ export async function getAuthorIds() { }, ); - console.log(Object.keys(authorFiles)); return Object.keys(authorFiles).map(authorPathToId); } diff --git a/src/lib/best-pratice.ts b/src/lib/best-pratice.ts index f881cd3cb..d3186aa1e 100644 --- a/src/lib/best-pratice.ts +++ b/src/lib/best-pratice.ts @@ -48,7 +48,7 @@ export async function getBestPracticeIds() { '/src/data/best-practices/*/*.md', { eager: true, - } + }, ); return Object.keys(bestPracticeFiles).map(bestPracticePathToId); @@ -64,7 +64,7 @@ export async function getAllBestPractices(): Promise { '/src/data/best-practices/*/*.md', { eager: true, - } + }, ); const bestPracticeFiles = Object.values(bestPracticeFilesMap); @@ -74,6 +74,38 @@ export async function getAllBestPractices(): Promise { })); return bestPracticeItems.sort( - (a, b) => a.frontmatter.order - b.frontmatter.order + (a, b) => a.frontmatter.order - b.frontmatter.order, ); } + +/** + * Gets the best practice file by ID + * + * @param id - Best practice file ID + * @returns BestPracticeFileType + */ + +export async function getBestPracticeById( + id: string, +): Promise { + const bestPracticeFilesMap = import.meta.glob( + '/src/data/best-practices/*/*.md', + { + eager: true, + }, + ); + + const bestPracticeFiles = Object.values(bestPracticeFilesMap); + const bestPracticeFile = bestPracticeFiles.find( + (bestPracticeFile) => bestPracticePathToId(bestPracticeFile.file) === id, + ); + + if (!bestPracticeFile) { + throw new Error(`Best practice with ID ${id} not found`); + } + + return { + ...bestPracticeFile, + id: bestPracticePathToId(bestPracticeFile.file), + }; +} diff --git a/src/lib/image.ts b/src/lib/image.ts new file mode 100644 index 000000000..fd211a44d --- /dev/null +++ b/src/lib/image.ts @@ -0,0 +1,12 @@ +import imageSize from 'image-size'; +import { readFile } from 'node:fs/promises'; + +export async function getLocalImageDimensions(path: string) { + try { + const imageBuffer = await readFile(path); + return imageSize(imageBuffer); + } catch (error) { + console.error(error, (error as Error)?.stack); + return null; + } +} diff --git a/src/lib/open-graph.ts b/src/lib/open-graph.ts new file mode 100644 index 000000000..17beff819 --- /dev/null +++ b/src/lib/open-graph.ts @@ -0,0 +1,8 @@ +type RoadmapOpenGraphQuery = { + group: 'roadmaps' | 'guides' | 'best-practices'; + resourceId: string; +}; + +export function getOpenGraphImageUrl(params: RoadmapOpenGraphQuery) { + return `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/og-images/${params.group}/${params.resourceId}.png`; +} diff --git a/src/pages/[roadmapId]/index.astro b/src/pages/[roadmapId]/index.astro index e30851491..55cf232b8 100644 --- a/src/pages/[roadmapId]/index.astro +++ b/src/pages/[roadmapId]/index.astro @@ -13,6 +13,7 @@ import { generateArticleSchema, generateFAQSchema, } from '../../lib/jsonld-schema'; +import { getOpenGraphImageUrl } from '../../lib/open-graph'; import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; export async function getStaticPaths() { @@ -55,14 +56,20 @@ if (roadmapData.schema) { if (roadmapFAQs.length) { jsonLdSchema.push(generateFAQSchema(roadmapFAQs)); } + +const ogImageUrl = + roadmapData?.seo?.ogImageUrl || + getOpenGraphImageUrl({ + group: 'roadmaps', + resourceId: roadmapId, + }); --- { + const authorDetails = await getAuthorById(authorId); + + return { + params: { authorId }, + props: { + authorDetails: authorDetails?.frontmatter || {}, + }, + }; + }), + ); +} + +export const GET: APIRoute = async function ({ params, request, props }) { + return new Response(JSON.stringify(props.authorDetails), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); +}; diff --git a/src/pages/backend/developer-skills.astro b/src/pages/backend/developer-skills.astro index 148a05762..020845a98 100644 --- a/src/pages/backend/developer-skills.astro +++ b/src/pages/backend/developer-skills.astro @@ -3,18 +3,25 @@ import GuideHeader from '../../components/GuideHeader.astro'; import MarkdownFile from '../../components/MarkdownFile.astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; import { getGuideById } from '../../lib/guide'; +import { getOpenGraphImageUrl } from '../../lib/open-graph'; const guideId = 'backend-developer-skills'; const guide = await getGuideById(guideId); -const { frontmatter: guideData } = guide; +const { frontmatter: guideData } = guide!; + +const ogImageUrl = getOpenGraphImageUrl({ + group: 'guides', + resourceId: guideId, +}); --- diff --git a/src/pages/backend/languages.astro b/src/pages/backend/languages.astro index 95a4a5a20..907b2ec41 100644 --- a/src/pages/backend/languages.astro +++ b/src/pages/backend/languages.astro @@ -3,11 +3,17 @@ import GuideHeader from '../../components/GuideHeader.astro'; import MarkdownFile from '../../components/MarkdownFile.astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; import { getGuideById } from '../../lib/guide'; +import { getOpenGraphImageUrl } from '../../lib/open-graph'; const guideId = 'backend-languages'; const guide = await getGuideById('backend-languages'); -const { frontmatter: guideData } = guide; +const { frontmatter: guideData } = guide!; + +const ogImageUrl = getOpenGraphImageUrl({ + group: 'guides', + resourceId: guideId, +}); --- - +
diff --git a/src/pages/best-practices/[bestPracticeId]/index.astro b/src/pages/best-practices/[bestPracticeId]/index.astro index ede49870b..fa7dce388 100644 --- a/src/pages/best-practices/[bestPracticeId]/index.astro +++ b/src/pages/best-practices/[bestPracticeId]/index.astro @@ -13,6 +13,7 @@ import { getAllBestPractices, } from '../../../lib/best-pratice'; import { generateArticleSchema } from '../../../lib/jsonld-schema'; +import { getOpenGraphImageUrl } from '../../../lib/open-graph'; export async function getStaticPaths() { const bestPractices = await getAllBestPractices(); @@ -52,12 +53,18 @@ if (bestPracticeData.schema) { }), ); } + +const ogImageUrl = getOpenGraphImageUrl({ + group: 'best-practices', + resourceId: bestPracticeId, +}); --- diff --git a/src/pages/pages.json.ts b/src/pages/pages.json.ts index 4eb74adc4..1fc6ebd7b 100644 --- a/src/pages/pages.json.ts +++ b/src/pages/pages.json.ts @@ -17,6 +17,7 @@ export async function GET() { id: roadmap.id, url: `/${roadmap.id}`, title: roadmap.frontmatter.briefTitle, + description: roadmap.frontmatter.briefDescription, group: 'Roadmaps', metadata: { tags: roadmap.frontmatter.tags, @@ -26,6 +27,7 @@ export async function GET() { id: bestPractice.id, url: `/best-practices/${bestPractice.id}`, title: bestPractice.frontmatter.briefTitle, + description: bestPractice.frontmatter.briefDescription, group: 'Best Practices', })), ...questionGroups.map((questionGroup) => ({ @@ -40,12 +42,14 @@ export async function GET() { ? guide.frontmatter.excludedBySlug : `/guides/${guide.id}`, title: guide.frontmatter.title, + description: guide.frontmatter.description, + authorId: guide.frontmatter.authorId, group: 'Guides', })), - ...videos.map((guide) => ({ - id: guide.id, - url: `/videos/${guide.id}`, - title: guide.frontmatter.title, + ...videos.map((video) => ({ + id: video.id, + url: `/videos/${video.id}`, + title: video.frontmatter.title, group: 'Videos', })), ]),