diff --git a/package-lock.json b/package-lock.json index 11f0a4a..6ea59e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,9 @@ "": { "name": "andwati-com", "devDependencies": { + "@resvg/resvg-js": "^2.6.2", + "satori": "^0.29.0", + "wawoff2": "^2.0.1", "wrangler": "^4" } }, @@ -1181,6 +1184,263 @@ "dev": true, "license": "MIT" }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "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" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@sindresorhus/is": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", @@ -1201,6 +1461,23 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", @@ -1208,6 +1485,23 @@ "dev": true, "license": "MIT" }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -1222,6 +1516,52 @@ "url": "https://opencollective.com/express" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz", + "integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1232,6 +1572,16 @@ "node": ">=8" } }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -1284,6 +1634,20 @@ "@esbuild/win32-x64": "0.28.1" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fflate": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.5.tgz", + "integrity": "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g==", + "dev": true, + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1299,6 +1663,19 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1309,6 +1686,17 @@ "node": ">=6" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, "node_modules/miniflare": { "version": "4.20260710.0", "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260710.0.tgz", @@ -1330,6 +1718,24 @@ "node": ">=22.0.0" } }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -1344,6 +1750,36 @@ "dev": true, "license": "MIT" }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/satori": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.29.0.tgz", + "integrity": "sha512-fPAfrwNaIQ7EsJLhY382STEqpczN6ZEH9NsKXDQgRcxUXNNPB73iAjSshzFwTaaScT68pxV0pmG6TBzDQ4sEGw==", + "dev": true, + "license": "MPL-2.0", + "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.0.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" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -1402,6 +1838,13 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -1415,6 +1858,13 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "dev": true, + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1443,6 +1893,31 @@ "pathe": "^2.0.3" } }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/wawoff2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wawoff2/-/wawoff2-2.0.1.tgz", + "integrity": "sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "woff2_compress.js": "bin/woff2_compress.js", + "woff2_decompress.js": "bin/woff2_decompress.js" + } + }, "node_modules/workerd": { "version": "1.20260710.1", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260710.1.tgz", @@ -1522,6 +1997,13 @@ } } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "dev": true, + "license": "MIT" + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", diff --git a/package.json b/package.json index 3ed9008..f3b0f85 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,15 @@ "name": "andwati-com", "private": true, "devDependencies": { + "@resvg/resvg-js": "^2.6.2", + "satori": "^0.29.0", + "wawoff2": "^2.0.1", "wrangler": "^4" }, "scripts": { "generate:llms": "node scripts/generate-llms.mjs", - "build": "npm run generate:llms && zola build", + "generate:og": "node scripts/generate-og-images.mjs", + "build": "npm run generate:llms && npm run generate:og && zola build", "deploy": "npm run build && wrangler deploy" } } diff --git a/scripts/generate-og-images.mjs b/scripts/generate-og-images.mjs new file mode 100644 index 0000000..238fe5e --- /dev/null +++ b/scripts/generate-og-images.mjs @@ -0,0 +1,237 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import satori from 'satori'; +import { Resvg } from '@resvg/resvg-js'; +import { decompress } from 'wawoff2'; + +const root = process.cwd(); +const postsDir = path.join(root, 'content', 'posts'); +const outDir = path.join(root, 'static', 'images', 'og'); + +const COLORS = { + bg: '#131311', + text: '#e9e7de', + muted: '#8b8a80', + accent: '#ff5a46', + border: 'rgba(233, 231, 222, 0.16)', +}; + +function tomlString(source, key) { + const match = source.match(new RegExp(`^${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, 'm')); + return match ? JSON.parse(match[1]) : undefined; +} + +function tomlStringArray(source, key) { + const match = source.match(new RegExp(`^${key}\\s*=\\s*\\[([^\\]]*)\\]`, 'm')); + if (!match) return []; + return [...match[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => JSON.parse(`"${m[1]}"`)); +} + +function frontMatter(source) { + return source.match(/^\+\+\+\s*\n([\s\S]*?)\n\+\+\+/)?.[1]; +} + +function taxonomies(source) { + return source.match(/^\[taxonomies\]\s*\n([\s\S]*?)(?:\n\[|\n?$)/m)?.[1] ?? ''; +} + +function markdownFiles(directory) { + return fs.readdir(directory, { withFileTypes: true }).then((entries) => + Promise.all( + entries.map((entry) => { + const location = path.join(directory, entry.name); + if (entry.isDirectory()) return markdownFiles(location); + return entry.isFile() && entry.name.endsWith('.md') ? [location] : []; + }), + ).then((lists) => lists.flat()), + ); +} + +function slugFor(file, metadata) { + const relative = path.relative(postsDir, file).replaceAll(path.sep, '/'); + const defaultSlug = relative.endsWith('/index.md') + ? relative.slice(0, -'/index.md'.length) + : relative.slice(0, -'.md'.length); + return tomlString(metadata, 'slug') ?? defaultSlug; +} + +async function loadFont(relPath) { + const woff2 = await fs.readFile(path.join(root, 'static', relPath)); + const ttf = await decompress(woff2); + return Buffer.from(ttf); +} + +function h(type, props, ...children) { + return { type, props: { ...props, children: children.flat().filter((c) => c !== null && c !== undefined) } }; +} + +function formatDate(date) { + return new Date(`${date}T00:00:00Z`).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC', + }); +} + +function card({ title, date, tags }) { + const stampTag = tags[0]; + const footerTags = tags.length > 4 ? `${tags.slice(0, 4).join(' · ')} ⋯` : tags.join(' · '); + const titleSize = title.length > 70 ? 46 : title.length > 45 ? 54 : 64; + + return h( + 'div', + { + style: { + width: '1200px', + height: '630px', + display: 'flex', + backgroundColor: COLORS.bg, + color: COLORS.text, + fontFamily: 'IBM Plex Serif', + }, + }, + h( + 'div', + { + style: { + flex: 1, + margin: '40px', + border: `1px solid ${COLORS.border}`, + borderRadius: '6px', + padding: '48px 56px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + }, + }, + h( + 'div', + { style: { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' } }, + h( + 'div', + { + style: { + display: 'flex', + fontFamily: 'JetBrains Mono', + fontWeight: 400, + fontSize: '22px', + color: COLORS.muted, + letterSpacing: '2px', + }, + }, + 'ANDWATI.COM · CASE FILE', + ), + stampTag + ? h( + 'div', + { + style: { + display: 'flex', + border: `2px solid ${COLORS.accent}`, + borderRadius: '4px', + padding: '8px 16px', + color: COLORS.accent, + fontFamily: 'JetBrains Mono', + fontWeight: 700, + fontSize: '20px', + letterSpacing: '2px', + textTransform: 'uppercase', + transform: 'rotate(3deg)', + }, + }, + stampTag, + ) + : null, + ), + h( + 'div', + { + style: { + display: 'flex', + fontFamily: 'IBM Plex Serif', + fontWeight: 700, + fontSize: `${titleSize}px`, + lineHeight: 1.15, + letterSpacing: '-1px', + color: COLORS.text, + overflow: 'hidden', + }, + }, + title, + ), + h( + 'div', + { + style: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + borderTop: `1px solid ${COLORS.border}`, + paddingTop: '24px', + }, + }, + h( + 'div', + { + style: { + display: 'flex', + fontFamily: 'JetBrains Mono', + fontWeight: 400, + fontSize: '20px', + color: COLORS.muted, + letterSpacing: '1px', + }, + }, + formatDate(date), + ), + h( + 'div', + { + style: { + display: 'flex', + fontFamily: 'JetBrains Mono', + fontWeight: 400, + fontSize: '20px', + color: COLORS.muted, + letterSpacing: '1px', + }, + }, + footerTags, + ), + ), + ), + ); +} + +const fonts = [ + { name: 'IBM Plex Serif', data: await loadFont('fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-700.woff2'), weight: 700, style: 'normal' }, + { name: 'JetBrains Mono', data: await loadFont('fonts/jetbrains-mono/JetBrainsMono-Regular.woff2'), weight: 400, style: 'normal' }, + { name: 'JetBrains Mono', data: await loadFont('fonts/jetbrains-mono/JetBrainsMono-Bold.woff2'), weight: 700, style: 'normal' }, +]; + +const files = (await markdownFiles(postsDir)).filter((file) => path.basename(file) !== '_index.md'); + +await fs.mkdir(outDir, { recursive: true }); + +let count = 0; +for (const file of files) { + const source = await fs.readFile(file, 'utf8'); + const metadata = frontMatter(source); + if (!metadata || /^draft\s*=\s*true$/m.test(metadata)) continue; + + const title = tomlString(metadata, 'title'); + const date = metadata.match(/^date\s*=\s*(\d{4}-\d{2}-\d{2})/m)?.[1]; + if (!title || !date) continue; + + const tags = tomlStringArray(taxonomies(metadata), 'tags'); + const slug = slugFor(file, metadata); + + const svg = await satori(card({ title, date, tags }), { width: 1200, height: 630, fonts }); + const png = new Resvg(svg).render().asPng(); + await fs.writeFile(path.join(outDir, `${slug}.png`), png); + count += 1; +} + +console.log(`Generated ${count} OG image${count === 1 ? '' : 's'} in ${path.relative(root, outDir)}/`); diff --git a/static/images/og/a-fresh-coat-of-paint.png b/static/images/og/a-fresh-coat-of-paint.png new file mode 100644 index 0000000..4b6db0d Binary files /dev/null and b/static/images/og/a-fresh-coat-of-paint.png differ diff --git a/static/images/og/from-c-to-machine-code.png b/static/images/og/from-c-to-machine-code.png new file mode 100644 index 0000000..326565e Binary files /dev/null and b/static/images/og/from-c-to-machine-code.png differ diff --git a/static/images/og/generate-django-secret-key.png b/static/images/og/generate-django-secret-key.png new file mode 100644 index 0000000..cc600e5 Binary files /dev/null and b/static/images/og/generate-django-secret-key.png differ diff --git a/static/images/og/hello-world.png b/static/images/og/hello-world.png new file mode 100644 index 0000000..b63d9d7 Binary files /dev/null and b/static/images/og/hello-world.png differ diff --git a/static/images/og/inertia.png b/static/images/og/inertia.png new file mode 100644 index 0000000..b1b05dd Binary files /dev/null and b/static/images/og/inertia.png differ diff --git a/static/images/og/install-postgresql-15-on-arch-manjaro-garuda-linux.png b/static/images/og/install-postgresql-15-on-arch-manjaro-garuda-linux.png new file mode 100644 index 0000000..b910415 Binary files /dev/null and b/static/images/og/install-postgresql-15-on-arch-manjaro-garuda-linux.png differ diff --git a/static/images/og/integrating-netlify.png b/static/images/og/integrating-netlify.png new file mode 100644 index 0000000..3545d22 Binary files /dev/null and b/static/images/og/integrating-netlify.png differ diff --git a/static/images/og/javascript-exporting-modules.png b/static/images/og/javascript-exporting-modules.png new file mode 100644 index 0000000..20e377b Binary files /dev/null and b/static/images/og/javascript-exporting-modules.png differ diff --git a/static/images/og/mmuctf2025.png b/static/images/og/mmuctf2025.png new file mode 100644 index 0000000..91d27df Binary files /dev/null and b/static/images/og/mmuctf2025.png differ diff --git a/static/images/og/obsidian-sync.png b/static/images/og/obsidian-sync.png new file mode 100644 index 0000000..7e92f83 Binary files /dev/null and b/static/images/og/obsidian-sync.png differ diff --git a/static/images/og/scriptctf2025.png b/static/images/og/scriptctf2025.png new file mode 100644 index 0000000..93f02b3 Binary files /dev/null and b/static/images/og/scriptctf2025.png differ diff --git a/static/images/og/self-hosting-gitea-and-mirroring-github.png b/static/images/og/self-hosting-gitea-and-mirroring-github.png new file mode 100644 index 0000000..70ac372 Binary files /dev/null and b/static/images/og/self-hosting-gitea-and-mirroring-github.png differ diff --git a/static/images/og/semantic-version-and-conventional-commits.png b/static/images/og/semantic-version-and-conventional-commits.png new file mode 100644 index 0000000..c45db31 Binary files /dev/null and b/static/images/og/semantic-version-and-conventional-commits.png differ diff --git a/static/images/og/subresource-integrity.png b/static/images/og/subresource-integrity.png new file mode 100644 index 0000000..13340d8 Binary files /dev/null and b/static/images/og/subresource-integrity.png differ diff --git a/static/images/og/the-single-byte-that-kills-your-exploit-understanding-endianness.png b/static/images/og/the-single-byte-that-kills-your-exploit-understanding-endianness.png new file mode 100644 index 0000000..2b29ecf Binary files /dev/null and b/static/images/og/the-single-byte-that-kills-your-exploit-understanding-endianness.png differ diff --git a/static/images/og/utterances-comments.png b/static/images/og/utterances-comments.png new file mode 100644 index 0000000..a273d2f Binary files /dev/null and b/static/images/og/utterances-comments.png differ diff --git a/static/images/og/vuejs-tailwind.png b/static/images/og/vuejs-tailwind.png new file mode 100644 index 0000000..720d6e6 Binary files /dev/null and b/static/images/og/vuejs-tailwind.png differ diff --git a/static/images/og/what-is-pwn.png b/static/images/og/what-is-pwn.png new file mode 100644 index 0000000..3564aca Binary files /dev/null and b/static/images/og/what-is-pwn.png differ diff --git a/static/images/og/wifi-hotspot-arch.png b/static/images/og/wifi-hotspot-arch.png new file mode 100644 index 0000000..7f6d54b Binary files /dev/null and b/static/images/og/wifi-hotspot-arch.png differ diff --git a/static/llms.txt b/static/llms.txt index 1782699..d8c2aad 100644 --- a/static/llms.txt +++ b/static/llms.txt @@ -14,6 +14,7 @@ This site contains articles in English, often with commands, code samples, worke ## Published articles +- [Self-Hosting Gitea and Automating GitHub Mirrors](https://andwati.com/posts/self-hosting-gitea-and-mirroring-github/): Self-Hosting Gitea and Automating GitHub Mirrors (and Everything Cloudflare Broke Along the Way) - [The One Thing Nobody Tells You About WiFi Hotspots on Arch Linux](https://andwati.com/posts/wifi-hotspot-arch/): A minimal Arch install is great until NetworkManager silently fails to find a binary you didn't know you needed - [From C to Machine Code: What Actually Runs](https://andwati.com/posts/from-c-to-machine-code/): Before diving into how to break a program, we need to understand what the computer is actually executing - [The Single Byte That Kills Your Exploit: Understanding Endianness](https://andwati.com/posts/the-single-byte-that-kills-your-exploit-understanding-endianness/): Problem: Computers speak different languages, like people diff --git a/templates/page.html b/templates/page.html index f93e95c..58f003b 100644 --- a/templates/page.html +++ b/templates/page.html @@ -9,7 +9,7 @@ {% block og_description %}{{ page.description | default(value=config.description) }}{% endblock og_description %} {% block og_url %}{{ page.permalink }}{% endblock og_url %} {% block og_type %}{% if "posts/_index.md" in page.ancestors %}article{% else %}website{% endif %}{% endblock og_type %} -{% block og_image %}{{ config.base_url }}{{ page.extra.image | default(value="/images/og-image.png") }}{% endblock og_image %} +{% block og_image %}{{ config.base_url }}{% if page.extra.image %}{{ page.extra.image }}{% elif "posts/_index.md" in page.ancestors %}/images/og/{{ page.slug }}.png{% else %}/images/og-image.png{% endif %}{% endblock og_image %} {% block og_extra %} {% if "posts/_index.md" in page.ancestors %} {% if page.date %}{% endif %} @@ -26,7 +26,7 @@ {# Twitter Card overrides #} {% block twitter_title %}{{ page.title }}{% endblock twitter_title %} {% block twitter_description %}{{ page.description | default(value=config.description) }}{% endblock twitter_description %} -{% block twitter_image %}{{ config.base_url }}{{ page.extra.image | default(value="/images/og-image.png") }}{% endblock twitter_image %} +{% block twitter_image %}{{ config.base_url }}{% if page.extra.image %}{{ page.extra.image }}{% elif "posts/_index.md" in page.ancestors %}/images/og/{{ page.slug }}.png{% else %}/images/og-image.png{% endif %}{% endblock twitter_image %} {% block head %} {% if "posts/_index.md" in page.ancestors %} @@ -42,7 +42,7 @@ }, "headline": {{ page.title | json_encode | safe }}, "description": {% if page.description %}{{ page.description | json_encode | safe }}{% else %}{{ config.description | json_encode | safe }}{% endif %}, - "image": "{{ config.base_url | safe }}{% if page.extra.image %}{{ page.extra.image | safe }}{% else %}/images/og-image.png{% endif %}", + "image": "{{ config.base_url | safe }}{% if page.extra.image %}{{ page.extra.image | safe }}{% elif "posts/_index.md" in page.ancestors %}/images/og/{{ page.slug | safe }}.png{% else %}/images/og-image.png{% endif %}", "url": "{{ page.permalink | safe }}", "inLanguage": "en-US", {% if page.date %}"datePublished": "{{ page.date | date(format="%Y-%m-%dT%H:%M:%S%:z") }}",{% endif %}