This commit is contained in:
2026-01-14 17:40:22 +01:00
parent a9b2c89636
commit 5247cdc5e9
8 changed files with 489 additions and 9 deletions

View File

@@ -3,11 +3,12 @@ import { defineConfig } from 'astro/config';
import react from '@astrojs/react'; import react from '@astrojs/react';
import mdx from '@astrojs/mdx'; import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: 'https://mintel.me', site: 'https://mintel.me',
integrations: [react(), mdx()], integrations: [react(), mdx(), sitemap()],
markdown: { markdown: {
syntaxHighlight: 'prism' syntaxHighlight: 'prism'
} }

259
package-lock.json generated
View File

@@ -10,10 +10,12 @@
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.3.13", "@astrojs/mdx": "^4.3.13",
"@astrojs/react": "^4.4.2", "@astrojs/react": "^4.4.2",
"@astrojs/sitemap": "^3.6.1",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^6.0.2",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"@types/react": "^19.2.8", "@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vercel/og": "^0.8.6",
"astro": "^5.16.8", "astro": "^5.16.8",
"ioredis": "^5.9.1", "ioredis": "^5.9.1",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
@@ -250,6 +252,17 @@
"react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/@astrojs/sitemap": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.6.1.tgz",
"integrity": "sha512-+o+TbxXqQJAOd+HxCjz/5RdAMrRFGjeuO+U6zddUuTO59WqMqXnsc8uveRiEr2Ff+3McZiEne7iG4J5cnuI6kA==",
"license": "MIT",
"dependencies": {
"sitemap": "^8.0.2",
"stream-replace-string": "^2.0.0",
"zod": "^3.25.76"
}
},
"node_modules/@astrojs/tailwind": { "node_modules/@astrojs/tailwind": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-6.0.2.tgz",
@@ -1641,6 +1654,15 @@
"integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@resvg/resvg-wasm": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@resvg/resvg-wasm/-/resvg-wasm-2.4.0.tgz",
"integrity": "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==",
"license": "MPL-2.0",
"engines": {
"node": ">= 10"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27", "version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -2069,6 +2091,22 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT" "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/@tailwindcss/typography": { "node_modules/@tailwindcss/typography": {
"version": "0.5.19", "version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
@@ -2229,6 +2267,15 @@
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/@types/sax": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
"integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2241,6 +2288,19 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@vercel/og": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/@vercel/og/-/og-0.8.6.tgz",
"integrity": "sha512-hBcWIOppZV14bi+eAmCZj8Elj8hVSUZJTpf1lgGBhVD85pervzQ1poM/qYfFUlPraYSZYP+ASg6To5BwYmUSGQ==",
"license": "MPL-2.0",
"dependencies": {
"@resvg/resvg-wasm": "2.4.0",
"satori": "0.16.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -2680,6 +2740,15 @@
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==",
"license": "MIT" "license": "MIT"
}, },
"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/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.9.14", "version": "2.9.14",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
@@ -2795,6 +2864,15 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"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/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001764", "version": "1.0.30001764",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
@@ -2947,6 +3025,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"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": { "node_modules/comma-separated-tokens": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -3006,6 +3090,36 @@
"uncrypto": "^0.1.3" "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.16",
"resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz",
"integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/css-select": { "node_modules/css-select": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -3022,6 +3136,17 @@
"url": "https://github.com/sponsors/fb55" "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": { "node_modules/css-tree": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
@@ -3414,6 +3539,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"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": { "node_modules/escape-string-regexp": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
@@ -3583,6 +3714,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/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -3974,6 +4111,18 @@
"url": "https://opencollective.com/unified" "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": { "node_modules/html-escaper": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -4287,6 +4436,16 @@
"url": "https://github.com/sponsors/antonk52" "url": "https://github.com/sponsors/antonk52"
} }
}, },
"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/lines-and-columns": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -5637,6 +5796,22 @@
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
"license": "MIT" "license": "MIT"
}, },
"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==",
"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-entities": { "node_modules/parse-entities": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
@@ -6472,6 +6647,37 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/satori": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/satori/-/satori-0.16.0.tgz",
"integrity": "sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==",
"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.16",
"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/satori/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/sax": { "node_modules/sax": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
@@ -6566,6 +6772,31 @@
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sitemap": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.2.tgz",
"integrity": "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ==",
"license": "MIT",
"dependencies": {
"@types/node": "^17.0.5",
"@types/sax": "^1.2.1",
"arg": "^5.0.0",
"sax": "^1.4.1"
},
"bin": {
"sitemap": "dist/cli.js"
},
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
}
},
"node_modules/sitemap/node_modules/@types/node": {
"version": "17.0.45",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"license": "MIT"
},
"node_modules/smol-toml": { "node_modules/smol-toml": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
@@ -6612,6 +6843,12 @@
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/stream-replace-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz",
"integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
"license": "MIT"
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@@ -6629,6 +6866,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/stringify-entities": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -7525,6 +7768,16 @@
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT" "license": "MIT"
}, },
"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==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unified": { "node_modules/unified": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -8084,6 +8337,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/zod": {
"version": "3.25.76", "version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",

View File

@@ -16,10 +16,12 @@
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.3.13", "@astrojs/mdx": "^4.3.13",
"@astrojs/react": "^4.4.2", "@astrojs/react": "^4.4.2",
"@astrojs/sitemap": "^3.6.1",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^6.0.2",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"@types/react": "^19.2.8", "@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vercel/og": "^0.8.6",
"astro": "^5.16.8", "astro": "^5.16.8",
"ioredis": "^5.9.1", "ioredis": "^5.9.1",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://mintel.me/sitemap.xml

View File

@@ -784,15 +784,15 @@ async function runAllTests() {
() => test('Download API endpoint exists', () => { () => test('Download API endpoint exists', () => {
const apiPath = path.join(process.cwd(), 'src/pages/api/download-zip.ts'); const apiPath = path.join(process.cwd(), 'src/pages/api/download-zip.ts');
const content = fs.readFileSync(apiPath, 'utf8'); const content = fs.readFileSync(apiPath, 'utf8');
if (!content.includes('export const POST')) { if (!content.includes('export const POST')) {
throw new Error('API endpoint missing POST handler'); throw new Error('API endpoint missing POST handler');
} }
if (!content.includes('export const GET')) { if (!content.includes('export const GET')) {
throw new Error('API endpoint missing GET handler'); throw new Error('API endpoint missing GET handler');
} }
if (!content.includes('FileExampleManager')) { if (!content.includes('FileExampleManager')) {
throw new Error('API endpoint does not use FileExampleManager'); throw new Error('API endpoint does not use FileExampleManager');
} }
@@ -883,21 +883,104 @@ async function runAllTests() {
'src/components/FileExample.astro', 'src/components/FileExample.astro',
'src/components/FileExamplesList.astro' 'src/components/FileExamplesList.astro'
]; ];
for (const component of components) { for (const component of components) {
const componentPath = path.join(process.cwd(), component); const componentPath = path.join(process.cwd(), component);
if (!fs.existsSync(componentPath)) { if (!fs.existsSync(componentPath)) {
throw new Error(`Component ${component} does not exist`); throw new Error(`Component ${component} does not exist`);
} }
const content = fs.readFileSync(componentPath, 'utf8'); const content = fs.readFileSync(componentPath, 'utf8');
// Check for common issues // Check for common issues
if (content.includes('children.trim') && !content.includes('children?.trim')) { if (content.includes('children.trim') && !content.includes('children?.trim')) {
throw new Error(`${component}: uses children.trim without null check`); throw new Error(`${component}: uses children.trim without null check`);
} }
} }
}), }),
() => test('OG image API route exists', () => {
const ogApiPath = path.join(process.cwd(), 'src/pages/api/og/[...slug].svg.ts');
if (!fs.existsSync(ogApiPath)) {
throw new Error('OG image API route does not exist');
}
const content = fs.readFileSync(ogApiPath, 'utf8');
if (!content.includes('export async function getStaticPaths')) {
throw new Error('OG API route missing getStaticPaths function');
}
if (!content.includes('export const GET')) {
throw new Error('OG API route missing GET handler');
}
if (!content.includes('blogPosts')) {
throw new Error('OG API route does not use blogPosts data');
}
}),
() => test('OG images are generated in dist', () => {
const ogDir = path.join(process.cwd(), 'dist/api/og');
if (!fs.existsSync(ogDir)) {
throw new Error('OG images directory does not exist in dist');
}
const files = fs.readdirSync(ogDir);
const expectedFiles = ['home.svg', 'first-note.svg', 'debugging-tips.svg', 'architecture-patterns.svg', 'docker-deployment.svg', 'embed-demo.svg'];
for (const expectedFile of expectedFiles) {
if (!files.includes(expectedFile)) {
throw new Error(`Missing OG image: ${expectedFile}`);
}
}
}),
() => test('OG images have correct content', () => {
const ogDir = path.join(process.cwd(), 'dist/api/og');
const homeOgPath = path.join(ogDir, 'home.svg');
if (!fs.existsSync(homeOgPath)) {
throw new Error('Home OG image does not exist');
}
const content = fs.readFileSync(homeOgPath, 'utf8');
if (!content.includes('<svg')) {
throw new Error('OG image is not valid SVG');
}
if (!content.includes('Marc Mintel')) {
throw new Error('OG image does not contain expected title');
}
if (!content.includes('mintel.me')) {
throw new Error('OG image does not contain site branding');
}
if (!content.includes('system-ui')) {
throw new Error('OG image does not use system fonts');
}
}),
() => test('BaseLayout uses OG images', () => {
const baseLayoutPath = path.join(process.cwd(), 'src/layouts/BaseLayout.astro');
const content = fs.readFileSync(baseLayoutPath, 'utf8');
if (!content.includes('ogImage')) {
throw new Error('BaseLayout does not set og:image meta tag');
}
if (!content.includes('twitterImage')) {
throw new Error('BaseLayout does not set twitter:image meta tag');
}
if (!content.includes('/api/og/')) {
throw new Error('BaseLayout does not use OG image API route');
}
}),
]; ];
// Run all sync tests // Run all sync tests

View File

@@ -22,9 +22,19 @@ import 'prismjs/components/prism-markdown';
interface Props { interface Props {
title: string; title: string;
description?: string; description?: string;
image?: string;
keywords?: string[];
canonicalUrl?: string;
} }
const { title, description = "Technical problem solver's blog - practical insights and learning notes" } = Astro.props; const { title, description = "Technical problem solver's blog - practical insights and learning notes", image, keywords, canonicalUrl } = Astro.props;
const siteUrl = 'https://mintel.me';
const currentUrl = canonicalUrl || (new URL(Astro.request.url)).pathname;
const fullUrl = `${siteUrl}${currentUrl}`;
const slug = currentUrl.split('/').filter(Boolean).pop() || 'home';
const ogImage = image || `${siteUrl}/api/og/${slug}.svg`;
const twitterImage = image || `${siteUrl}/api/og/${slug}.svg`;
const keywordsString = keywords ? keywords.join(', ') : '';
--- ---
<!DOCTYPE html> <!DOCTYPE html>
@@ -34,7 +44,24 @@ const { title, description = "Technical problem solver's blog - practical insigh
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} | Marc Mintel</title> <title>{title} | Marc Mintel</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
{keywordsString && <meta name="keywords" content={keywordsString} />}
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<link rel="canonical" href={fullUrl} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={fullUrl} />
<meta property="og:title" content={`${title} | Marc Mintel`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content="Marc Mintel" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={fullUrl} />
<meta property="twitter:title" content={`${title} | Marc Mintel`} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={twitterImage} />
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">

View File

@@ -0,0 +1,72 @@
import type { APIRoute } from 'astro';
import { blogPosts } from '../../../data/blogPosts';
export async function getStaticPaths() {
const paths = blogPosts.map(post => ({
params: { slug: post.slug }
}));
// Add home page
paths.push({ params: { slug: 'home' } });
return paths;
}
export const GET: APIRoute = async ({ params }) => {
const slug = params.slug;
let title: string;
let description: string;
// Handle home page
if (slug === 'home') {
title = 'Marc Mintel';
description = 'Technical problem solver\'s blog - practical insights and learning notes';
} else {
// Find the blog post
const post = blogPosts.find(p => p.slug === slug);
// Default content if no post found
title = (post?.title || 'Marc Mintel').replace(/[<>&'"]/g, '');
description = (post?.description || 'Technical problem solver\'s blog - practical insights and learning notes').slice(0, 100).replace(/[<>&'"]/g, '');
}
// Create SVG with typographic design matching the site
const svg = `<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
.title { font-family: system-ui, -apple-system, sans-serif; font-size: 48px; font-weight: 700; fill: #1e293b; letter-spacing: -0.025em; }
.description { font-family: system-ui, -apple-system, sans-serif; font-size: 24px; font-weight: 400; fill: #64748b; }
.branding { font-family: system-ui, -apple-system, sans-serif; font-size: 18px; font-weight: 500; fill: #94a3b8; }
</style>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="#ffffff"/>
<!-- Title -->
<text x="60" y="200" class="title">
${title}
</text>
<!-- Description -->
<text x="60" y="280" class="description">
${description}
</text>
<!-- Site branding -->
<text x="60" y="580" class="branding">
mintel.me
</text>
<!-- Decorative accent -->
<rect x="1000" y="60" width="120" height="4" fill="#3b82f6" rx="2"/>
</svg>`;
return new Response(svg, {
headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
};

View File

@@ -33,7 +33,12 @@ const showFileExamples = post.tags?.some(tag =>
); );
--- ---
<BaseLayout title={post.title} description={post.description}> <BaseLayout
title={post.title}
description={post.description}
keywords={post.tags}
canonicalUrl={`/blog/${post.slug}`}
/>
<!-- Top navigation bar with back button and clap counter --> <!-- Top navigation bar with back button and clap counter -->
<nav class="fixed top-0 left-0 right-0 z-40 bg-white/80 backdrop-blur-md border-b border-slate-200 transition-all duration-300" id="top-nav"> <nav class="fixed top-0 left-0 right-0 z-40 bg-white/80 backdrop-blur-md border-b border-slate-200 transition-all duration-300" id="top-nav">
<div class="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between"> <div class="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
@@ -266,6 +271,33 @@ const showFileExamples = post.tags?.some(tag =>
</section> </section>
</main> </main>
<!-- Structured Data for SEO -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.title,
"description": post.description,
"author": {
"@type": "Person",
"name": "Marc Mintel",
"url": "https://mintel.me"
},
"publisher": {
"@type": "Person",
"name": "Marc Mintel",
"url": "https://mintel.me"
},
"datePublished": post.date,
"dateModified": post.date,
"mainEntityOfPage": {
"@type": "WebPage",
"@id": `https://mintel.me/blog/${post.slug}`
},
"url": `https://mintel.me/blog/${post.slug}`,
"keywords": post.tags.join(", "),
"articleSection": post.tags[0] || "Technology"
})} />
<script> <script>
// Reading progress bar with smooth gradient // Reading progress bar with smooth gradient
function updateReadingProgress() { function updateReadingProgress() {