diff --git a/package-lock.json b/package-lock.json index 4a7df24..2fd0a36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,15 @@ "dependencies": { "@astrojs/rss": "^4.0.14", "@astrojs/sitemap": "^3.6.0", + "@resvg/resvg-js": "^2.6.2", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", "astro": "^5.16.6", "markdown-it": "^14.1.0", "mermaid": "^11.12.2", + "react": "^19.2.3", "sanitize-html": "^2.17.0", + "satori": "^0.19.1", "sharp": "^0.34.5", "tailwindcss": "^4.1.18" }, @@ -1212,6 +1215,221 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "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==", + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", @@ -1593,6 +1811,22 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, + "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==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -2581,6 +2815,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2725,6 +2968,12 @@ "node": ">=6" } }, + "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==", + "license": "MIT" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2793,6 +3042,36 @@ "uncrypto": "^0.1.3" } }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -2809,6 +3088,17 @@ "url": "https://github.com/sponsors/fb55" } }, + "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==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", @@ -3616,6 +3906,15 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -3688,6 +3987,12 @@ "@esbuild/win32-x64": "0.25.12" } }, + "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==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -3762,6 +4067,12 @@ } } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/flattie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", @@ -4036,6 +4347,18 @@ "url": "https://opencollective.com/unified" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -4538,6 +4861,25 @@ "url": "https://opencollective.com/parcel" } }, + "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==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -5657,6 +5999,16 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "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==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/parse-latin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", @@ -5797,6 +6149,12 @@ "node": ">=4" } }, + "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==", + "license": "MIT" + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -5844,6 +6202,15 @@ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -6188,6 +6555,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/satori": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.19.1.tgz", + "integrity": "sha512-/XaT/JiWLfNlgjlQdde4wXB1/6F+FEze9c3OW2QIH0ywsfOrY57YOetgESWyOFHW3JfEQ6dJAo2U9Xwb7+DDAw==", + "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/sax": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", @@ -6351,6 +6740,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -7163,6 +7558,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT" + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 346ccee..8815011 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,21 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", - "new": "node scripts/new-post.js" + "new": "node scripts/new-post.js", + "cover": "node scripts/generate-cover.js" }, "dependencies": { "@astrojs/rss": "^4.0.14", "@astrojs/sitemap": "^3.6.0", + "@resvg/resvg-js": "^2.6.2", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", "astro": "^5.16.6", "markdown-it": "^14.1.0", "mermaid": "^11.12.2", + "react": "^19.2.3", "sanitize-html": "^2.17.0", + "satori": "^0.19.1", "sharp": "^0.34.5", "tailwindcss": "^4.1.18" }, diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..8b5bfea Binary files /dev/null and b/public/images/logo.png differ diff --git a/scripts/assets/calvin.png b/scripts/assets/calvin.png new file mode 100644 index 0000000..e5d14af Binary files /dev/null and b/scripts/assets/calvin.png differ diff --git a/scripts/assets/inter-bold.woff b/scripts/assets/inter-bold.woff new file mode 100644 index 0000000..32ab5ea Binary files /dev/null and b/scripts/assets/inter-bold.woff differ diff --git a/scripts/assets/inter-italic.woff b/scripts/assets/inter-italic.woff new file mode 100644 index 0000000..35e0a7f --- /dev/null +++ b/scripts/assets/inter-italic.woff @@ -0,0 +1,11 @@ + + + + + Error 404 (Not Found)!!1 + + +

404. That’s an error. +

The requested URL /s/inter/v13/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7SUc.woff was not found on this server. That’s all we know. diff --git a/scripts/assets/inter-regular.woff b/scripts/assets/inter-regular.woff new file mode 100644 index 0000000..53c8e9a Binary files /dev/null and b/scripts/assets/inter-regular.woff differ diff --git a/scripts/assets/logo.png b/scripts/assets/logo.png new file mode 100644 index 0000000..8b5bfea Binary files /dev/null and b/scripts/assets/logo.png differ diff --git a/scripts/assets/lora-italic.woff b/scripts/assets/lora-italic.woff new file mode 100644 index 0000000..11c76b2 Binary files /dev/null and b/scripts/assets/lora-italic.woff differ diff --git a/scripts/assets/lora-italic.woff2 b/scripts/assets/lora-italic.woff2 new file mode 100644 index 0000000..81e6cf8 Binary files /dev/null and b/scripts/assets/lora-italic.woff2 differ diff --git a/scripts/generate-cover.js b/scripts/generate-cover.js new file mode 100644 index 0000000..406bf97 --- /dev/null +++ b/scripts/generate-cover.js @@ -0,0 +1,484 @@ +import satori from "satori"; +import { Resvg } from "@resvg/resvg-js"; +import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs"; +import { readFile } from "fs/promises"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { createInterface } from "readline"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Dimensions +const WIDTH = 1920; +const HEIGHT = 1080; + +// Brand colors +const COLORS = { + blue: "#3B8DBD", + darkBlue: "#1a5a7a", + darkerBlue: "#0d3d54", + darkestBlue: "#0a2a3a", + orange: "#FFB833", + white: "#FFFFFF", + gray: "#6D6E71", +}; + +// Load image as base64 +function loadImageBase64(filename) { + const imagePath = join(__dirname, "assets", filename); + const buffer = readFileSync(imagePath); + return `data:image/png;base64,${buffer.toString("base64")}`; +} + +// Load fonts from local files +function loadFont() { + const fontPath = join(__dirname, "assets", "inter-regular.woff"); + return readFileSync(fontPath); +} + +function loadFontBold() { + const fontPath = join(__dirname, "assets", "inter-bold.woff"); + return readFileSync(fontPath); +} + +function loadFontLoraItalic() { + const fontPath = join(__dirname, "assets", "lora-italic.woff"); + return readFileSync(fontPath); +} + +// Create the cover template +function createTemplate(title, subtitle, logoBase64, calvinBase64) { + return { + type: "div", + props: { + style: { + width: WIDTH, + height: HEIGHT, + display: "flex", + flexDirection: "column", + position: "relative", + overflow: "hidden", + backgroundColor: COLORS.darkestBlue, + }, + children: [ + // Background layers (angled shapes) + { + type: "div", + props: { + style: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + display: "flex", + }, + children: [ + // Darkest layer (base) + { + type: "div", + props: { + style: { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + backgroundColor: COLORS.darkestBlue, + }, + }, + }, + // Dark blue angled layer + { + type: "div", + props: { + style: { + position: "absolute", + top: 0, + left: "15%", + width: "100%", + height: "100%", + backgroundColor: COLORS.darkerBlue, + transform: "skewX(-15deg)", + }, + }, + }, + // Medium blue angled layer + { + type: "div", + props: { + style: { + position: "absolute", + top: 0, + left: "30%", + width: "100%", + height: "100%", + backgroundColor: COLORS.darkBlue, + transform: "skewX(-15deg)", + }, + }, + }, + // Light blue angled layer + { + type: "div", + props: { + style: { + position: "absolute", + top: 0, + left: "45%", + width: "100%", + height: "100%", + backgroundColor: COLORS.blue, + transform: "skewX(-15deg)", + }, + }, + }, + ], + }, + }, + // Decorative code brackets + { + type: "div", + props: { + style: { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + fontSize: 840, + fontWeight: 700, + color: COLORS.white, + opacity: 0.06, + letterSpacing: -10, + }, + children: "", + }, + }, + // Content container + { + type: "div", + props: { + style: { + position: "relative", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + width: "100%", + height: "100%", + padding: 60, + }, + children: [ + // Title section (top) + { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + gap: 16, + paddingLeft: 20, + }, + children: [ + // Main title (max 2 lines to avoid overlapping Calvin) + { + type: "div", + props: { + style: { + fontSize: 92, + fontWeight: 700, + color: COLORS.white, + lineHeight: 1.1, + maxWidth: "90%", + textShadow: "0 4px 12px rgba(0,0,0,0.4)", + }, + children: title, + }, + }, + // Subtitle (can wrap to multiple lines, width constrained to avoid Calvin) + subtitle + ? { + type: "div", + props: { + style: { + fontSize: 78, + fontWeight: 400, + color: COLORS.orange, + lineHeight: 1.2, + maxWidth: "85%", + textShadow: "0 3px 6px rgba(0,0,0,0.7), 0 1px 2px rgba(0,0,0,0.5)", + }, + children: subtitle, + }, + } + : null, + ].filter(Boolean), + }, + }, + // Logo (bottom left) + { + type: "img", + props: { + src: logoBase64, + width: 320, + height: 166, + style: { + objectFit: "contain", + opacity: 0.4, + }, + }, + }, + ], + }, + }, + // Calvin mascot (absolute positioned, bottom right) + { + type: "img", + props: { + src: calvinBase64, + width: 700, + height: 875, + style: { + position: "absolute", + bottom: -40, + right: 0, + objectFit: "contain", + transform: "scaleX(-1)", + }, + }, + }, + ], + }, + }; +} + +// Generate the cover image +async function generateCover(title, subtitle) { + const font = loadFont(); + const fontBold = loadFontBold(); + const fontLoraItalic = loadFontLoraItalic(); + const logoBase64 = loadImageBase64("logo.png"); + const calvinBase64 = loadImageBase64("calvin.png"); + + const template = createTemplate(title, subtitle, logoBase64, calvinBase64); + + const svg = await satori(template, { + width: WIDTH, + height: HEIGHT, + fonts: [ + { name: "Inter", data: font, weight: 400, style: "normal" }, + { name: "Inter", data: fontBold, weight: 700, style: "normal" }, + { name: "Lora", data: fontLoraItalic, weight: 400, style: "italic" }, + ], + }); + + const resvg = new Resvg(svg, { + fitTo: { mode: "width", value: WIDTH }, + }); + + return Buffer.from(resvg.render().asPng()); +} + +// Parse frontmatter from markdown file +function parseFrontmatter(content) { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) return {}; + + const frontmatter = {}; + const lines = match[1].split("\n"); + + for (const line of lines) { + const colonIndex = line.indexOf(":"); + if (colonIndex > 0) { + const key = line.slice(0, colonIndex).trim(); + let value = line.slice(colonIndex + 1).trim(); + // Remove quotes + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } + frontmatter[key] = value; + } + } + + return frontmatter; +} + +// Find all blog posts +function findBlogPosts() { + const blogDir = join(process.cwd(), "src", "content", "blog"); + const posts = []; + + const years = readdirSync(blogDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + + for (const year of years) { + const yearDir = join(blogDir, year); + const slugs = readdirSync(yearDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + + for (const slug of slugs) { + const postDir = join(yearDir, slug); + const indexPath = join(postDir, "index.md"); + + if (existsSync(indexPath)) { + posts.push({ + year, + slug, + path: postDir, + indexPath, + }); + } + } + } + + return posts; +} + +// Interactive post selection +async function selectPost(posts) { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const ask = (q) => + new Promise((resolve) => rl.question(q, (a) => resolve(a.trim()))); + + console.log("\nAvailable posts without cover images:\n"); + + const postsWithoutCover = posts.filter( + (p) => !existsSync(join(p.path, "cover.png")) + ); + + if (postsWithoutCover.length === 0) { + console.log("All posts have cover images!"); + rl.close(); + return null; + } + + postsWithoutCover.forEach((p, i) => { + console.log(` ${i + 1}. ${p.year}/${p.slug}`); + }); + + console.log(`\n 0. Generate for a specific path`); + console.log(` a. Generate all missing covers\n`); + + const choice = await ask("Select an option: "); + rl.close(); + + if (choice.toLowerCase() === "a") { + return postsWithoutCover; + } + + const index = parseInt(choice, 10); + if (index === 0) { + return "manual"; + } + + if (index > 0 && index <= postsWithoutCover.length) { + return [postsWithoutCover[index - 1]]; + } + + return null; +} + +// Generate cover for a single post +async function generateForPost(post) { + const content = readFileSync(post.indexPath, "utf-8"); + const frontmatter = parseFrontmatter(content); + + if (!frontmatter.title) { + console.log(` Skipping ${post.slug}: No title found`); + return false; + } + + // Use title as-is, optional subtitle from frontmatter + const title = frontmatter.title; + const subtitle = frontmatter.subtitle || null; + + console.log(` Generating: ${post.year}/${post.slug}`); + console.log(` Title: ${title}`); + if (subtitle) console.log(` Subtitle: ${subtitle}`); + + const png = await generateCover(title, subtitle); + const outputPath = join(post.path, "cover.png"); + writeFileSync(outputPath, png); + + console.log(` Saved: ${outputPath}\n`); + return true; +} + +// Main CLI +async function main() { + const args = process.argv.slice(2); + + // Direct path provided + if (args[0]) { + const postPath = args[0]; + const indexPath = join(postPath, "index.md"); + + if (!existsSync(indexPath)) { + console.error(`Error: No index.md found at ${postPath}`); + process.exit(1); + } + + const post = { + path: postPath, + indexPath, + slug: postPath.split(/[/\\]/).pop(), + year: postPath.split(/[/\\]/).slice(-2)[0], + }; + + await generateForPost(post); + return; + } + + // Interactive mode + const posts = findBlogPosts(); + const selection = await selectPost(posts); + + if (!selection) { + console.log("No selection made."); + return; + } + + if (selection === "manual") { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + const path = await new Promise((resolve) => + rl.question("Enter post path: ", resolve) + ); + rl.close(); + + const indexPath = join(path, "index.md"); + if (!existsSync(indexPath)) { + console.error(`Error: No index.md found at ${path}`); + process.exit(1); + } + + await generateForPost({ + path, + indexPath, + slug: path.split(/[/\\]/).pop(), + year: path.split(/[/\\]/).slice(-2)[0], + }); + return; + } + + // Generate for selected posts + console.log(`\nGenerating ${selection.length} cover(s)...\n`); + + for (const post of selection) { + await generateForPost(post); + } + + console.log("Done!"); +} + +// Export for use in new-post.js +export { generateCover, parseFrontmatter }; + +main().catch(console.error); diff --git a/scripts/new-post.js b/scripts/new-post.js index 4bd2eda..6863fcd 100644 --- a/scripts/new-post.js +++ b/scripts/new-post.js @@ -1,6 +1,11 @@ import { createInterface } from "readline"; import { mkdir, writeFile } from "fs/promises"; import { existsSync } from "fs"; +import { spawn } from "child_process"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); const rl = createInterface({ input: process.stdin, @@ -29,6 +34,19 @@ function getISODate() { return now.toISOString().replace("Z", "").split(".")[0] + offsetStr; } +async function generateCover(postDir) { + return new Promise((resolve, reject) => { + const scriptPath = join(__dirname, "generate-cover.js"); + const child = spawn("node", [scriptPath, postDir], { + stdio: "inherit", + }); + child.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`Cover generation failed with code ${code}`)); + }); + }); +} + async function main() { console.log("\n📝 New Blog Post\n"); @@ -49,7 +67,14 @@ async function main() { : []; const description = await ask("Description (optional): "); - const includeCover = await ask("Include cover image? (y/N): "); + + const includeCover = await ask("Generate cover image? (y/N): "); + const wantsCover = includeCover.toLowerCase() === "y"; + + let subtitle = ""; + if (wantsCover) { + subtitle = await ask("Cover subtitle (optional): "); + } rl.close(); @@ -72,9 +97,8 @@ async function main() { frontmatter.push(`description: "${description}"`); } - const wantsCover = includeCover.toLowerCase() === "y"; - if (wantsCover) { - frontmatter.push("image: ./cover.png"); + if (subtitle) { + frontmatter.push(`subtitle: "${subtitle}"`); } frontmatter.push("---", "", "Your content here...", ""); @@ -83,8 +107,15 @@ async function main() { await writeFile(`${dir}/index.md`, frontmatter.join("\n")); console.log(`\n✅ Created: ${dir}/index.md`); + if (wantsCover) { - console.log(`📷 Don't forget to add cover.png to ${dir}/`); + console.log(`\n🎨 Generating cover image...`); + try { + await generateCover(dir); + } catch (err) { + console.log(`⚠️ Cover generation failed: ${err.message}`); + console.log(` You can run 'npm run cover' later to generate it.`); + } } } diff --git a/src/components/PostCard.astro b/src/components/PostCard.astro index 32fa0e1..9b13018 100644 --- a/src/components/PostCard.astro +++ b/src/components/PostCard.astro @@ -4,6 +4,7 @@ import type { ImageMetadata } from 'astro'; interface Props { title: string; + subtitle?: string; slug: string; date: Date; description?: string; @@ -11,7 +12,7 @@ interface Props { image?: ImageMetadata; } -const { title, slug, date, description, categories, image } = Astro.props; +const { title, subtitle, slug, date, description, categories, image } = Astro.props; const formattedDate = date.toLocaleDateString('en-US', { year: 'numeric', @@ -21,20 +22,22 @@ const formattedDate = date.toLocaleDateString('en-US', { }); --- -

+
{image && ( {title} )} -
+
{categories.map(category => ( ))}
-

+

{title}

+ {subtitle && ( +

{subtitle}

+ )} {description && ( -

{description}

+

{description}

)} - +
diff --git a/src/content/blog/2026/introducing-dtvem/index.md b/src/content/blog/2026/introducing-dtvem/index.md index 9edb841..ceead88 100644 --- a/src/content/blog/2026/introducing-dtvem/index.md +++ b/src/content/blog/2026/introducing-dtvem/index.md @@ -2,7 +2,8 @@ title: "Introducing the Developer Tools Virtual Environment Manager!" date: "2026-01-05T12:00:00-05:00" categories: [golang, cli, python, node, ruby] -description: "A unified, cross-platform runtime version manager that actually works on Windows" +description: "Tired of juggling nvm, pyenv, and rbenv? dtvem is a unified, cross-platform runtime version manager written in Go that manages Python, Node.js, and Ruby - and actually works on Windows without WSL." +subtitle: "One tool to rule all your runtimes!" blueskyPostId: 3mbpawpkn4z26 --- diff --git a/src/content/blog/2026/introducing-git-ranger-extension/cover.png b/src/content/blog/2026/introducing-git-ranger-extension/cover.png new file mode 100644 index 0000000..6d4d136 Binary files /dev/null and b/src/content/blog/2026/introducing-git-ranger-extension/cover.png differ diff --git a/src/content/blog/2026/introducing-git-ranger-extension/index.md b/src/content/blog/2026/introducing-git-ranger-extension/index.md new file mode 100644 index 0000000..ad69010 --- /dev/null +++ b/src/content/blog/2026/introducing-git-ranger-extension/index.md @@ -0,0 +1,52 @@ +--- +title: "Introducing the 'Git Ranger' Visual Studio extension!" +date: "2026-01-16T12:00:00-05:00" +categories: [dotnet, csharp, extensibility, visualstudio] +description: "Git Ranger brings enhanced Git tooling to Visual Studio 2022 and 2026. See who modified each line, when, and why - right in your editor. Inline annotations, gutter margins, author color coding, age heat maps, and more. Inspired by GitLens for VS Code." +subtitle: "Enhanced Git tooling for Visual Studio!" +--- + +Introducing "[Git Ranger](https://marketplace.visualstudio.com/items?itemName=CodingWithCalvin.VS-GitRanger)", an extension for Visual Studio 2022 (and 2026!) that brings enhanced Git tooling directly into your editor. If you've ever used GitLens in VS Code, this extension draws inspiration from that experience and aims to bring similar capabilities to Visual Studio. + +## The Vision + +Git Ranger is designed to be a comprehensive Git companion for Visual Studio developers. The goal is to surface Git information where you need it, when you need it - without breaking your flow or switching context. + +## Current Features + +The initial release focuses on inline Git information: + +- **Inline Annotations** - See who last modified each line, when, and why - right in your editor +- **Gutter Margin** - A visual indicator in the left margin showing commit history at a glance +- **Theme-Adaptive Colors** - 12 vibrant colors that automatically adjust for dark and light themes +- **Author Color Coding** - Each contributor gets their own distinctive color +- **Age Heat Map Mode** - Optional visualization showing relative age of changes +- **Interactive Tooltips** - Hover to see full commit details +- **Copy Commit SHA** - Quick access to commit hashes via click or menu command + +## Customization + +Git Ranger is highly configurable. Head to **Tools → Options → Git Ranger** where you can adjust display options, color modes, date formats, caching behavior, and more. + +## What's Coming + +The roadmap is packed with planned features: + +- **Git CodeLens** for methods and classes +- **File History** and **Line History** tool windows +- **Visual Git Graph** with styling and interactions +- **Status bar indicator** +- **Branch and Tag management** tool windows +- **Commit Details** and **Contributors** views +- **Diff integration** +- **Remote provider integrations** (GitHub, GitLab, etc.) +- **Git Command Palette** +- **Stash management** +- **Interactive rebase editor** +- **AI-powered commit message generation** + +You can check out the full [issue list on GitHub](https://github.com/CodingWithCalvin/VS-GitRanger/issues) to see everything that's planned and track progress. + +## Get It Now + +Feel free to check it out on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=CodingWithCalvin.VS-GitRanger), and let me know if you have any suggestions! It's [open source on GitHub](https://github.com/CodingWithCalvin/VS-GitRanger), so issues and PRs are happily accepted if you're into that sort of thing. diff --git a/src/content/blog/2026/introducing-rnr-a-zero-dependency-task-runner/index.md b/src/content/blog/2026/introducing-rnr-a-zero-dependency-task-runner/index.md index 67e0a89..f95fa65 100644 --- a/src/content/blog/2026/introducing-rnr-a-zero-dependency-task-runner/index.md +++ b/src/content/blog/2026/introducing-rnr-a-zero-dependency-task-runner/index.md @@ -2,7 +2,8 @@ title: "Introducing rnr - A Zero-Dependency Task Runner" date: "2026-01-12T12:00:00-05:00" categories: [rust, oss, cli] -description: "Meet rnr (pronounced 'runner') - a cross-platform task runner that lives inside your repo. Contributors clone and run, zero friction." +description: "Most task runners require Node.js, Make, or other global dependencies. rnr (pronounced 'runner') is different - the binaries live inside your repository. Contributors clone your project and immediately run tasks. No setup, no version mismatches, zero friction." +subtitle: "Clone the repo and run, zero friction!" blueskyPostId: "3mcap7y23wl2f" --- diff --git a/src/content/blog/2026/introducing-the-visual-studio-code-mcp-server/index.md b/src/content/blog/2026/introducing-the-visual-studio-code-mcp-server/index.md index 85bf396..035928e 100644 --- a/src/content/blog/2026/introducing-the-visual-studio-code-mcp-server/index.md +++ b/src/content/blog/2026/introducing-the-visual-studio-code-mcp-server/index.md @@ -2,7 +2,8 @@ title: "Introducing the Visual Studio Code MCP Server!" date: "2026-01-15T12:00:00-05:00" categories: [typescript, extensibility, vscode, ai, mcp] -description: "A VS Code extension that exposes the IDE's language intelligence through the Model Context Protocol, enabling AI assistants to leverage IntelliSense, Go to Definition, Find References, and more." +description: "VS Code has incredible language intelligence - IntelliSense, Go to Definition, Find References - but it's locked inside the IDE. This extension bridges that gap, exposing VS Code's language server capabilities through MCP so your AI tools can tap into the same semantic understanding." +subtitle: "Giving AI access to VS Code's language intelligence!" blueskyPostId: "3mci66mistd2p" --- diff --git a/src/content/blog/2026/introducing-the-visual-studio-mcp-server/index.md b/src/content/blog/2026/introducing-the-visual-studio-mcp-server/index.md index 0195fb7..cdcf085 100644 --- a/src/content/blog/2026/introducing-the-visual-studio-mcp-server/index.md +++ b/src/content/blog/2026/introducing-the-visual-studio-mcp-server/index.md @@ -2,7 +2,8 @@ title: "Introducing the Visual Studio MCP Server!" date: "2026-01-14T12:00:00-05:00" categories: [dotnet, csharp, extensibility, visualstudio, ai, mcp] -description: "A Visual Studio extension that exposes IDE features through the Model Context Protocol, enabling AI assistants to interact with Visual Studio programmatically." +description: "JetBrains is shipping MCP servers for their IDEs. Visual Studio isn't. So I wrote one. This extension exposes Visual Studio features through the Model Context Protocol - solutions, documents, builds, and more - enabling AI assistants like Claude to interact with your IDE." +subtitle: "AI meets Visual Studio through MCP!" blueskyPostId: "3mcfc5n5pxc2s" --- diff --git a/src/content/blog/2026/introducing-the-visual-studio-toolbox/index.md b/src/content/blog/2026/introducing-the-visual-studio-toolbox/index.md index 078674a..cba3467 100644 --- a/src/content/blog/2026/introducing-the-visual-studio-toolbox/index.md +++ b/src/content/blog/2026/introducing-the-visual-studio-toolbox/index.md @@ -2,7 +2,8 @@ title: "Introducing the Visual Studio Toolbox!" date: "2026-01-02T12:00:00-05:00" categories: [dotnet, csharp, visualstudio, winui] -description: "Mission Control for your Visual Studio Installations, inspired by JetBrains Toolbox" +description: "Inspired by JetBrains Toolbox, this Windows system tray app automatically detects all your Visual Studio installations - 2019, 2022, 2026, experimental hives - and puts them one click away. No more hunting through the Start menu." +subtitle: "Mission Control for Visual Studio!" blueskyPostId: 3mbhgd6et4b26 --- diff --git a/src/content/blog/2026/introducing-vscwhere/index.md b/src/content/blog/2026/introducing-vscwhere/index.md index cfd1c91..4e11e1e 100644 --- a/src/content/blog/2026/introducing-vscwhere/index.md +++ b/src/content/blog/2026/introducing-vscwhere/index.md @@ -2,7 +2,8 @@ title: "Introducing vscwhere!" date: "2026-01-08T12:00:00-05:00" categories: [rust, cli, vscode] -description: "A CLI tool for locating Visual Studio Code installations on Windows, inspired by Microsoft's vswhere" +description: "Need to find VS Code installations for CI/CD scripts or automation? vscwhere is a CLI tool written in Rust that locates all Visual Studio Code installations on Windows - Stable and Insiders builds alike. Same familiar interface as Microsoft's vswhere." +subtitle: "Like vswhere, but for VS Code!" blueskyPostId: 3mbwhet7cbl24 --- diff --git a/src/content/blog/2026/sdk-style-projects-for-your-visual-studio-extensions/index.md b/src/content/blog/2026/sdk-style-projects-for-your-visual-studio-extensions/index.md index e2eb74d..c390c09 100644 --- a/src/content/blog/2026/sdk-style-projects-for-your-visual-studio-extensions/index.md +++ b/src/content/blog/2026/sdk-style-projects-for-your-visual-studio-extensions/index.md @@ -2,7 +2,8 @@ title: "SDK-style Projects for your Visual Studio Extensions!" date: "2026-01-01T12:00:00-05:00" categories: [dotnet, csharp, vsix] -description: "Remember that MSBuild SDK post from last week? Well, I actually built something with it - an SDK that brings modern project files to Visual Studio extension development." +description: "Tired of hundreds of lines of XML in your VSIX project files? CodingWithCalvin.VsixSdk is an MSBuild SDK that brings modern SDK-style .csproj files to Visual Studio extension development. Clean project files, dotnet CLI support, and no more XML soup." +subtitle: "Modern project files for VSIX development!" blueskyPostId: 3mbezw2qfgt2m --- diff --git a/src/content/config.ts b/src/content/config.ts index ab7a634..570b5ee 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -5,6 +5,7 @@ const blog = defineCollection({ loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }), schema: ({ image }) => z.object({ title: z.string(), + subtitle: z.string().optional(), date: z.coerce.date(), categories: z.array(z.string()), description: z.string().optional(), diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index cec7c8d..3c0017f 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -41,7 +41,7 @@ const twitterHandle = "@_CalvinAllen"; - + {title} | {siteName} @@ -86,11 +86,10 @@ const twitterHandle = "@_CalvinAllen";
- - - Coding With Calvin + + Coding With Calvin -