From 5247cdc5e93736f78a3c28a94c9cc56d07e2f6f7 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 14 Jan 2026 17:40:22 +0100 Subject: [PATCH] seo --- astro.config.mjs | 3 +- package-lock.json | 259 ++++++++++++++++++++++++++++++ package.json | 2 + public/robots.txt | 4 + scripts/smoke-test.ts | 95 ++++++++++- src/layouts/BaseLayout.astro | 29 +++- src/pages/api/og/[...slug].svg.ts | 72 +++++++++ src/pages/blog/[slug].astro | 34 +++- 8 files changed, 489 insertions(+), 9 deletions(-) create mode 100644 public/robots.txt create mode 100644 src/pages/api/og/[...slug].svg.ts diff --git a/astro.config.mjs b/astro.config.mjs index 351a986..12727db 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -3,11 +3,12 @@ import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import mdx from '@astrojs/mdx'; +import sitemap from '@astrojs/sitemap'; // https://astro.build/config export default defineConfig({ site: 'https://mintel.me', - integrations: [react(), mdx()], + integrations: [react(), mdx(), sitemap()], markdown: { syntaxHighlight: 'prism' } diff --git a/package-lock.json b/package-lock.json index 6bec471..0326b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "dependencies": { "@astrojs/mdx": "^4.3.13", "@astrojs/react": "^4.4.2", + "@astrojs/sitemap": "^3.6.1", "@astrojs/tailwind": "^6.0.2", "@types/ioredis": "^4.28.10", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", + "@vercel/og": "^0.8.6", "astro": "^5.16.8", "ioredis": "^5.9.1", "lucide-react": "^0.468.0", @@ -250,6 +252,17 @@ "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": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-6.0.2.tgz", @@ -1641,6 +1654,15 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "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": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2069,6 +2091,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/@tailwindcss/typography": { "version": "0.5.19", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", @@ -2229,6 +2267,15 @@ "@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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2241,6 +2288,19 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "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": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2680,6 +2740,15 @@ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", "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": { "version": "2.9.14", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", @@ -2795,6 +2864,15 @@ "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": { "version": "1.0.30001764", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", @@ -2947,6 +3025,12 @@ "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": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -3006,6 +3090,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.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": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -3022,6 +3136,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", @@ -3414,6 +3539,12 @@ "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": { "version": "5.0.0", "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": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3974,6 +4111,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", @@ -4287,6 +4436,16 @@ "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": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5637,6 +5796,22 @@ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "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": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", @@ -6472,6 +6647,37 @@ "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": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", @@ -6566,6 +6772,31 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "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": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", @@ -6612,6 +6843,12 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "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": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -6629,6 +6866,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", @@ -7525,6 +7768,16 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "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": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -8084,6 +8337,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 b390a62..237f5be 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,12 @@ "dependencies": { "@astrojs/mdx": "^4.3.13", "@astrojs/react": "^4.4.2", + "@astrojs/sitemap": "^3.6.1", "@astrojs/tailwind": "^6.0.2", "@types/ioredis": "^4.28.10", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", + "@vercel/og": "^0.8.6", "astro": "^5.16.8", "ioredis": "^5.9.1", "lucide-react": "^0.468.0", diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..aa8cbee --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://mintel.me/sitemap.xml \ No newline at end of file diff --git a/scripts/smoke-test.ts b/scripts/smoke-test.ts index 279ddd9..f8800e4 100644 --- a/scripts/smoke-test.ts +++ b/scripts/smoke-test.ts @@ -784,15 +784,15 @@ async function runAllTests() { () => test('Download API endpoint exists', () => { const apiPath = path.join(process.cwd(), 'src/pages/api/download-zip.ts'); const content = fs.readFileSync(apiPath, 'utf8'); - + if (!content.includes('export const POST')) { throw new Error('API endpoint missing POST handler'); } - + if (!content.includes('export const GET')) { throw new Error('API endpoint missing GET handler'); } - + if (!content.includes('FileExampleManager')) { throw new Error('API endpoint does not use FileExampleManager'); } @@ -883,21 +883,104 @@ async function runAllTests() { 'src/components/FileExample.astro', 'src/components/FileExamplesList.astro' ]; - + for (const component of components) { const componentPath = path.join(process.cwd(), component); if (!fs.existsSync(componentPath)) { throw new Error(`Component ${component} does not exist`); } - + const content = fs.readFileSync(componentPath, 'utf8'); - + // Check for common issues if (content.includes('children.trim') && !content.includes('children?.trim')) { 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(' 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 diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 2a10c69..195d4e0 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -22,9 +22,19 @@ import 'prismjs/components/prism-markdown'; interface Props { title: 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(', ') : ''; --- @@ -34,7 +44,24 @@ const { title, description = "Technical problem solver's blog - practical insigh {title} | Marc Mintel + {keywordsString && } + + + + + + + + + + + + + + + + diff --git a/src/pages/api/og/[...slug].svg.ts b/src/pages/api/og/[...slug].svg.ts new file mode 100644 index 0000000..aab2f00 --- /dev/null +++ b/src/pages/api/og/[...slug].svg.ts @@ -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 = ` + + + + + + + + + + ${title} + + + + + ${description} + + + + + mintel.me + + + + + `; + + return new Response(svg, { + headers: { + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }); +}; \ No newline at end of file diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index 637fe29..ea73789 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -33,7 +33,12 @@ const showFileExamples = post.tags?.some(tag => ); --- - +