diff --git a/package.json b/package.json index 7f859ea1..ff884aa2 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@neoconfetti/svelte": "^2.2.2", "@octokit/graphql-schema": "^15.26.0", "@prgm/sveltekit-progress-bar": "^3.0.2", + "@resvg/resvg-js": "^2.6.2", "@shikijs/langs": "^3.13.0", "@shikijs/rehype": "^3.13.0", "@shikijs/themes": "^3.13.0", @@ -57,6 +58,8 @@ "remark-gemoji": "^8.0.0", "remark-github": "^12.0.0", "runed": "^0.34.0", + "satori": "^0.18.3", + "satori-html": "^0.3.2", "semver": "^7.7.2", "shiki": "^3.13.0", "svelte": "^5.39.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb3deb42..776a3efa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@prgm/sveltekit-progress-bar': specifier: ^3.0.2 version: 3.0.2(@sveltejs/kit@2.38.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.7)(vite@7.1.7(@types/node@24.6.0)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.7)(vite@7.1.7(@types/node@24.6.0)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.7) + '@resvg/resvg-js': + specifier: ^2.6.2 + version: 2.6.2 '@shikijs/langs': specifier: ^3.13.0 version: 3.13.0 @@ -137,6 +140,12 @@ importers: runed: specifier: ^0.34.0 version: 0.34.0(@sveltejs/kit@2.38.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.7)(vite@7.1.7(@types/node@24.6.0)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.7)(vite@7.1.7(@types/node@24.6.0)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.7) + satori: + specifier: ^0.18.3 + version: 0.18.3 + satori-html: + specifier: ^0.3.2 + version: 0.3.2 semver: specifier: ^7.7.2 version: 7.7.2 @@ -640,6 +649,82 @@ packages: '@sveltejs/kit': ^2.0.0 svelte: ^5.0.0 + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -783,6 +868,11 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -1132,6 +1222,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} @@ -1162,6 +1256,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1218,6 +1315,23 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-gradient-parser@0.0.17: + resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==} + engines: {node: '>=16'} + + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1259,6 +1373,10 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + emoji-regex-xs@2.0.1: + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} + engines: {node: '>=10.0.0'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1278,6 +1396,9 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1391,6 +1512,9 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1495,6 +1619,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -1660,6 +1788,9 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -1904,10 +2035,16 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -1966,6 +2103,9 @@ packages: resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} engines: {node: '>=4'} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2172,6 +2312,13 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + satori-html@0.3.2: + resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==} + + satori@0.18.3: + resolution: {integrity: sha512-T3DzWNmnrfVmk2gCIlAxLRLbGkfp3K7TyRva+Byyojqu83BNvnMeqVeYRdmUw4TKCsyH4RiQ/KuF/I4yEzgR5A==} + engines: {node: '>=16'} + sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} @@ -2220,6 +2367,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -2317,6 +2467,9 @@ packages: resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -2373,12 +2526,18 @@ packages: engines: {node: '>=14.17'} hasBin: true + ultrahtml@1.6.0: + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} undici-types@7.13.0: resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -2525,6 +2684,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -2967,6 +3129,57 @@ snapshots: '@sveltejs/kit': 2.38.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.39.7)(vite@7.1.7(@types/node@24.6.0)(jiti@2.6.0)(lightningcss@1.30.1)))(svelte@5.39.7)(vite@7.1.7(@types/node@24.6.0)(jiti@2.6.0)(lightningcss@1.30.1)) svelte: 5.39.7 + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@rollup/pluginutils@5.3.0(rollup@4.52.3)': dependencies: '@types/estree': 1.0.8 @@ -3083,6 +3296,11 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + '@standard-schema/spec@1.0.0': {} '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': @@ -3434,6 +3652,8 @@ snapshots: balanced-match@1.0.2: {} + base64-js@0.0.8: {} + before-after-hook@4.0.0: {} bindings@1.5.0: @@ -3468,6 +3688,8 @@ snapshots: callsites@3.1.0: {} + camelize@1.0.1: {} + ccount@2.0.1: {} chalk@4.1.2: @@ -3511,6 +3733,20 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + + css-gradient-parser@0.0.17: {} + + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + cssesc@3.0.0: {} debug@4.4.3: @@ -3537,6 +3773,8 @@ snapshots: eastasianwidth@0.2.0: {} + emoji-regex-xs@2.0.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3577,6 +3815,8 @@ snapshots: '@esbuild/win32-ia32': 0.25.10 '@esbuild/win32-x64': 0.25.10 + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -3712,6 +3952,8 @@ snapshots: fflate@0.4.8: {} + fflate@0.7.4: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -3855,6 +4097,8 @@ snapshots: property-information: 7.1.0 space-separated-tokens: 2.0.2 + hex-rgb@4.3.0: {} + html-void-elements@3.0.0: {} https-proxy-agent@7.0.6: @@ -3979,6 +4223,11 @@ snapshots: lilconfig@2.1.0: {} + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + locate-character@3.0.0: {} locate-path@6.0.0: @@ -4396,10 +4645,17 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@0.2.9: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -4444,6 +4700,8 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-value-parser@4.2.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -4639,6 +4897,24 @@ snapshots: dependencies: mri: 1.2.0 + satori-html@0.3.2: + dependencies: + ultrahtml: 1.6.0 + + satori@0.18.3: + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-gradient-parser: 0.0.17 + css-to-react-native: 3.2.0 + emoji-regex-xs: 2.0.1 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-layout: 3.2.1 + sax@1.4.1: {} schema-dts@1.1.5: {} @@ -4688,6 +4964,8 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string.prototype.codepointat@0.2.1: {} + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -4807,6 +5085,8 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + tiny-inflate@1.0.3: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -4855,10 +5135,17 @@ snapshots: typescript@5.9.2: {} + ultrahtml@1.6.0: {} + uncrypto@0.1.3: {} undici-types@7.13.0: {} + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -4986,6 +5273,8 @@ snapshots: yocto-queue@0.1.0: {} + yoga-layout@3.2.1: {} + zimmerframe@1.1.4: {} zwitch@2.0.4: {} diff --git a/src/routes/[pid=pid]/[org]/[repo]/[id=number]/+page.ts b/src/routes/[pid=pid]/[org]/[repo]/[id=number]/+page.ts index 4c281781..e4d34e86 100644 --- a/src/routes/[pid=pid]/[org]/[repo]/[id=number]/+page.ts +++ b/src/routes/[pid=pid]/[org]/[repo]/[id=number]/+page.ts @@ -1,10 +1,25 @@ import type { MetaTagsProps } from "svelte-meta-tags"; -export function load({ data }) { +export function load({ data, url }) { return { ...data, pageMetaTags: Object.freeze({ - title: `Detail of ${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}` + title: `Detail of ${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}`, + openGraph: { + images: [ + { + get url() { + const ogUrl = new URL("og", url.origin); + ogUrl.searchParams.set("title", data.item.info.title); + ogUrl.searchParams.set( + "description", + `${data.itemMetadata.org}/${data.itemMetadata.repo}#${data.itemMetadata.id}` + ); + return ogUrl.href; + } + } + ] + } }) satisfies MetaTagsProps }; } diff --git a/src/routes/devlog/v2/+page.ts b/src/routes/devlog/v2/+page.ts index 8e7031e1..02bc2fa7 100644 --- a/src/routes/devlog/v2/+page.ts +++ b/src/routes/devlog/v2/+page.ts @@ -1,12 +1,20 @@ import type { MetaTagsProps } from "svelte-meta-tags"; -export function load() { +export function load({ url }) { return { pageMetaTags: Object.freeze({ title: "v2 • Devlog", description: "The development blog of Svelte Changelog", - twitter: { - description: "The development blog of Svelte Changelog" + openGraph: { + images: [ + { + get url() { + const ogUrl = new URL("og", url.origin); + ogUrl.searchParams.set("title", "v2 • Devlog"); + return ogUrl.href; + } + } + ] } }) satisfies MetaTagsProps }; diff --git a/src/routes/og/+server.ts b/src/routes/og/+server.ts new file mode 100644 index 00000000..2f43b137 --- /dev/null +++ b/src/routes/og/+server.ts @@ -0,0 +1,78 @@ +import { render } from "svelte/server"; +import { read } from "$app/server"; +import DMSerifDisplay from "@fontsource/dm-serif-display/files/dm-serif-display-latin-400-normal.woff"; +import Pretendard from "@fontsource/pretendard/files/pretendard-latin-400-normal.woff"; +import PretendardSemibold from "@fontsource/pretendard/files/pretendard-latin-600-normal.woff"; +import { Resvg } from "@resvg/resvg-js"; +import satori from "satori"; +import { html } from "satori-html"; +import type { RequestHandler } from "./$types"; +import Thumbnail from "./Thumbnail.svelte"; +import { OG_HEIGHT, OG_WIDTH } from "./constants"; + +const sansFont = read(Pretendard).arrayBuffer(); +const sansFontSemibold = read(PretendardSemibold).arrayBuffer(); +const displayFont = read(DMSerifDisplay).arrayBuffer(); + +// Sources: https://github.com/huggingface/chat-ui/blob/ebeff50ac0ac4367a8e1a32b46dcc5ac2e8fc43f/src/routes/assistant/%5BassistantId%5D/thumbnail.png/%2Bserver.ts#L44-L82 +// https://geoffrich.net/posts/svelte-social-image/ +export const GET: RequestHandler = async ({ url }) => { + const renderedComponent = render(Thumbnail, { + props: { + title: url.searchParams.get("title") ?? "", + description: url.searchParams.get("description") ?? undefined + } + }); + + const reactLike = html(`${renderedComponent.body}`); + + const svg = await satori(reactLike, { + width: OG_WIDTH, + height: OG_HEIGHT, + fonts: [ + { + name: "SansFont", + data: await sansFont, + weight: 400, + style: "normal" + }, + { + name: "SansFontSemibold", + data: await sansFontSemibold, + weight: 600, + style: "normal" + }, + { + name: "DisplayFont", + data: await displayFont, + weight: 400, + style: "normal" + } + ] + }); + + const png = new Resvg(svg, { + fitTo: { + mode: "original" + } + }) + .render() + .asPng(); + + // `png` is not usable directly inside `new Response()` for some reason, TS says + let bodyData; + if (png instanceof ArrayBuffer) { + bodyData = png; + } else if (png.buffer instanceof ArrayBuffer) { + bodyData = png.buffer; + } else { + bodyData = new Uint8Array(png); + } + + return new Response(bodyData, { + headers: { + "Content-Type": "image/png", + "Cache-Control": "public, max-age=31536000, immutable" + } + }); +}; diff --git a/src/routes/og/Thumbnail.svelte b/src/routes/og/Thumbnail.svelte new file mode 100644 index 00000000..de82f34b --- /dev/null +++ b/src/routes/og/Thumbnail.svelte @@ -0,0 +1,36 @@ + + +
+ Svelte +
+
+ Svelte + + Svelte + Changelog + +
+
+

{title}

+ {#if description} +

{description}

+ {/if} +
+
+
diff --git a/src/routes/og/constants.ts b/src/routes/og/constants.ts new file mode 100644 index 00000000..394043d8 --- /dev/null +++ b/src/routes/og/constants.ts @@ -0,0 +1,4 @@ +// Source: https://vercel.com/docs/og-image-generation#technical-details + +export const OG_WIDTH = 1_200; +export const OG_HEIGHT = 630; diff --git a/src/routes/package/[...package]/+page.ts b/src/routes/package/[...package]/+page.ts index 174b3846..ebb3ea21 100644 --- a/src/routes/package/[...package]/+page.ts +++ b/src/routes/package/[...package]/+page.ts @@ -1,10 +1,25 @@ import type { MetaTagsProps } from "svelte-meta-tags"; -export function load({ data }) { +export function load({ data, url }) { return { ...data, pageMetaTags: Object.freeze({ - title: data.currentPackage.pkg.name + title: data.currentPackage.pkg.name, + openGraph: { + images: [ + { + get url() { + const ogUrl = new URL("og", url.origin); + ogUrl.searchParams.set("title", data.currentPackage.pkg.name); + ogUrl.searchParams.set( + "description", + `${data.currentPackage.repoOwner}/${data.currentPackage.repoName}` + ); + return ogUrl.href; + } + } + ] + } }) satisfies MetaTagsProps }; } diff --git a/src/routes/packages/+page.ts b/src/routes/packages/+page.ts index 2c8d4a40..65819210 100644 --- a/src/routes/packages/+page.ts +++ b/src/routes/packages/+page.ts @@ -1,10 +1,21 @@ import type { MetaTagsProps } from "svelte-meta-tags"; -export function load({ data }) { +export function load({ data, url }) { return { ...data, pageMetaTags: Object.freeze({ - title: "All Packages" + title: "All Packages", + openGraph: { + images: [ + { + get url() { + const ogUrl = new URL("og", url.origin); + ogUrl.searchParams.set("title", "All Packages"); + return ogUrl.href; + } + } + ] + } }) satisfies MetaTagsProps }; } diff --git a/src/routes/tracker/[org]/[repo]/+page.ts b/src/routes/tracker/[org]/[repo]/+page.ts index 1266ca75..eae7c6a8 100644 --- a/src/routes/tracker/[org]/[repo]/+page.ts +++ b/src/routes/tracker/[org]/[repo]/+page.ts @@ -1,10 +1,21 @@ import type { MetaTagsProps } from "svelte-meta-tags"; -export function load({ data, params }) { +export function load({ data, params, url }) { return { ...data, pageMetaTags: Object.freeze({ - title: `Tracker for ${params.org}/${params.repo}` + title: `Tracker for ${params.org}/${params.repo}`, + openGraph: { + images: [ + { + get url() { + const ogUrl = new URL("og", url.origin); + ogUrl.searchParams.set("title", `Tracker • ${params.org}/${params.repo}`); + return ogUrl.href; + } + } + ] + } }) satisfies MetaTagsProps }; }