diff --git a/package-lock.json b/package-lock.json index aa78c9c8..35e9cd22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,7 @@ "astro": "^6.3.3", "astro-og-canvas": "0.11.1", "astro-vtbot": "2.1.12", - "baseline-browser-mapping": "2.10.29", + "baseline-browser-mapping": "2.10.30", "canvas-confetti": "1.9.4", "confusing-browser-globals": "1.0.11", "cross-env": "10.1.0", @@ -93,13 +93,13 @@ "email-validator": "2.0.4", "embla-carousel": "8.6.0", "embla-carousel-autoplay": "8.6.0", - "eslint": "10.3.0", + "eslint": "10.4.0", "eslint-plugin-astro": "1.7.0", "eslint-plugin-import-lite": "0.6.0", "eslint-plugin-jsdoc": "62.9.0", "eslint-plugin-security": "4.0.0", "eslint-plugin-yml": "3.3.2", - "focus-trap": "8.2.0", + "focus-trap": "8.2.1", "globals": "17.6.0", "gsap": "3.15.0", "happy-dom": "20.9.0", @@ -110,10 +110,10 @@ "husky": "9.1.7", "install": "0.13.0", "is-whitespace-character": "2.0.1", - "isomorphic-git": "1.37.6", - "js-cookie": "3.0.5", + "isomorphic-git": "1.38.0", + "js-cookie": "3.0.7", "jsdom": "29.1.1", - "libphonenumber-js": "1.13.1", + "libphonenumber-js": "1.13.2", "lighthouse": "13.3.0", "lit": "3.3.3", "markdownlint-cli2": "0.22.1", @@ -129,11 +129,11 @@ "playwright-lighthouse": "4.0.0", "postcss": "8.5.14", "postcss-html": "1.8.1", - "preact": "10.29.1", + "preact": "10.29.2", "prettier": "3.8.3", "prettier-plugin-astro": "0.14.1", "pubsub-js": "1.9.5", - "puppeteer": "24.43.1", + "puppeteer": "25.0.2", "qr-code-styling": "1.9.2", "rehype-accessible-emojis": "0.3.2", "rehype-external-links": "3.0.0", @@ -185,7 +185,7 @@ "unist-util-is": "6.0.1", "unist-util-visit": "5.1.0", "uuid": "14.0.0", - "vercel": "54.0.0", + "vercel": "54.1.0", "vite": "7.3.2", "vite-plugin-pwa": "1.3.0", "vitest": "4.1.6", @@ -479,24 +479,6 @@ "node": ">=14.0.0" } }, - "node_modules/@astrojs/compiler-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.2.0.tgz", @@ -3352,9 +3334,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "license": "Apache-2.0", "dependencies": { "@eslint/core": "^1.2.1" @@ -4726,6 +4708,24 @@ "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@neon-rs/load": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz", @@ -5725,24 +5725,6 @@ "node": ">=14.0.0" } }, - "node_modules/@oxc-transform/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-transform/binding-win32-arm64-msvc": { "version": "0.111.0", "resolved": "https://registry.npmjs.org/@oxc-transform/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.111.0.tgz", @@ -6314,24 +6296,6 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.1", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.1.tgz", @@ -9461,12 +9425,12 @@ } }, "node_modules/@vercel/backends": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.6.0.tgz", - "integrity": "sha512-Fw+9s72dMowyL4HNl4IYCbk0ubvfDDXpgaPENNx49+k248LU9LoLjDOek7gTiAfwiiQK3RViN3327ZLCe0GF/A==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.7.0.tgz", + "integrity": "sha512-BPmjGQw6G6tq6Qym9DvvtLHYhFsrjswIJ5K1vPjdZUzmagfzCK9nmDqqNjOnDUmR3tS6SpCxcYwWaM4USy6Aag==", "license": "Apache-2.0", "dependencies": { - "@vercel/build-utils": "13.24.0", + "@vercel/build-utils": "13.25.0", "@vercel/nft": "1.5.0", "execa": "3.2.0", "fs-extra": "11.1.0", @@ -9529,9 +9493,9 @@ } }, "node_modules/@vercel/build-utils": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.24.0.tgz", - "integrity": "sha512-eJuceaJvHlyyOc9a0jFL989ObKYkF/5FNYSiocHeM/KltwTJ+aC6I4SHUL0zkYf6a1CE5DJXiwY/QCX985PchA==", + "version": "13.25.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.25.0.tgz", + "integrity": "sha512-p2wqxi2I95T+g/+uP+Dc/uq2PApW7F9RbE/Vvwp28JY8SoCGHNUs2pxigttgaNDNF6IlUEMOTz+eJvsXToV/1w==", "license": "Apache-2.0", "dependencies": { "@vercel/python-analysis": "0.11.1", @@ -9552,12 +9516,12 @@ "license": "MIT" }, "node_modules/@vercel/cervel": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.1.4.tgz", - "integrity": "sha512-7r/xbkaqIhb/XVunZdQFPhK7BfebSKl8Ty+o4uqiZ8PIZUd5g1/e/OtdMXaLUZGdrpbg98UZwExI2WUrguZ2rA==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.1.5.tgz", + "integrity": "sha512-TLYex/J/sgaJvYTQDjcmDnIhkk2R3Maloyvegd7bpmmK55WkxtBajAkwXVE0x03Msh9KJMeg+LaMkpBndRy4Ig==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.6.0" + "@vercel/backends": "0.7.0" }, "bin": { "cervel": "bin/cervel.mjs" @@ -9592,12 +9556,12 @@ } }, "node_modules/@vercel/elysia": { - "version": "0.1.77", - "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.77.tgz", - "integrity": "sha512-EPrjJGVgfqRvCzYFK5WhMgxBJD2xHCfGMQUMZ6bglCFj+CIj2sljHgxCaRl2Q8q8+haOKdAZUNowJxQ63FzsxA==", + "version": "0.1.78", + "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.78.tgz", + "integrity": "sha512-mnlkm9eiNkvT0gPnAQ1757G9Luj63fDCe+DwgTj+I1sEu58YOj+/NxcKxF73Pw03iAcinPwzqkNOS60QTWRSGQ==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/static-config": "3.3.0" } }, @@ -9608,14 +9572,14 @@ "license": "Apache-2.0" }, "node_modules/@vercel/express": { - "version": "0.1.87", - "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.87.tgz", - "integrity": "sha512-bNUTcA1mNYzU0G6cohQnk53qHE3eqyhvoUxdMcrzWA5U/yU0i35/BW8Z4Ju+R6ghRdjXzzmGmFgTSLnVD9Y23g==", + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.88.tgz", + "integrity": "sha512-EI5M7X8pVEGq7coGBNtr7BEnz9ijGLR6CgwvvV/wcLd1LHwhBDm6y30wUzqExHSaBAq+HqUTAgWtNP/vkTu4KA==", "license": "Apache-2.0", "dependencies": { - "@vercel/cervel": "0.1.4", + "@vercel/cervel": "0.1.5", "@vercel/nft": "1.5.0", - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/static-config": "3.3.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", @@ -9657,12 +9621,12 @@ } }, "node_modules/@vercel/fastify": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.80.tgz", - "integrity": "sha512-EpkOtRlnjVVdMANzqUToloUloOMSHs6FHJLcmCSJDLYaCNzlt+wIHCo6b0p7fjB43zZt/DmDx06jh/XWzxOAog==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.81.tgz", + "integrity": "sha512-6wEMsAaSnWgJxVZV9pR75yMZn7wAtGLe4pukofdAPrJu/m4I+OzCc93xcBHc7DzoxF9GA9K5chTbgsVxaB1oXg==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/static-config": "3.3.0" } }, @@ -9822,13 +9786,13 @@ } }, "node_modules/@vercel/gatsby-plugin-vercel-builder": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.2.4.tgz", - "integrity": "sha512-6VZHmRK1jL1XyCVmypH0mGdJR3j1V0JzLwDFqmQnfoYfWb7c5rsQGzxqyvv3T6yqHC0udUiQ2jYAiEiKZ7i+Gg==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.2.5.tgz", + "integrity": "sha512-uOqW3u9uqeC8oqhI6q1A+UJutiCulRXtjezSVz3FbpgIEW9p06XRHIVXfJf1i/ZXMT/fChGpCLXzJmfxf2dr3A==", "license": "Apache-2.0", "dependencies": { "@sinclair/typebox": "0.25.24", - "@vercel/build-utils": "13.24.0", + "@vercel/build-utils": "13.25.0", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" @@ -10306,29 +10270,29 @@ } }, "node_modules/@vercel/go": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@vercel/go/-/go-3.6.0.tgz", - "integrity": "sha512-60gQxuQBfuGQCPdp1zmG3bfWncj+MRDm9KNogKeu4cA1uC2kGwUoCI6vr8kpGTv62v3gwSLbcnJ3RsqOqxb3KA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@vercel/go/-/go-3.7.0.tgz", + "integrity": "sha512-Z5yahC9dP30jWXR/AfqY+mKrm98MhUVOoyDv6JTwddRI0kcqf4eTYQkKVBDKlZGkvmpkKXaO9bG7G5a8sk2w1A==", "license": "Apache-2.0" }, "node_modules/@vercel/h3": { - "version": "0.1.86", - "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.86.tgz", - "integrity": "sha512-7p9IDxE7X2Jg1W78GlG1dE0zZDZFCl2SIm2698pY0qfsK72le1UCfpf24FJbfE1T1HDZ4cEVHSJO4o3QTsU0qw==", + "version": "0.1.87", + "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.87.tgz", + "integrity": "sha512-wQ24c848UZYKEltmUKmCBHPRhtppHRlOreEyaJLC3FDhLUBLAfhH+53jqFGaKdbkp0HwukH9GdRc5GcMtCpavw==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/static-config": "3.3.0" } }, "node_modules/@vercel/hono": { - "version": "0.2.80", - "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.80.tgz", - "integrity": "sha512-mbzluA7u/Vz7+pPlfTRcaBsy/0z1yH1Pv1Ibyd18pcFMLGyLhLRYjWdC2J95UFHSeYm9tWFDlTsVl2/jH9fPew==", + "version": "0.2.81", + "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.81.tgz", + "integrity": "sha512-DoiMAqembXYopq1OiYk1EBu+os92qc/amrbULMOjOO/+Rmh/yaIkrBoOT4RNntxdwImDixXdURxgvUJ0RZ0kJA==", "license": "Apache-2.0", "dependencies": { "@vercel/nft": "1.5.0", - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/static-config": "3.3.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", @@ -10380,22 +10344,22 @@ } }, "node_modules/@vercel/koa": { - "version": "0.1.60", - "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.60.tgz", - "integrity": "sha512-LRKBSyj7kEieuP7brAgM35/4m13od5bYGsFOS5gGvJxsr0vA7v4gzGN8mbrMy0PoG7DHzU2bC4Pc4IrGUakaZg==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.61.tgz", + "integrity": "sha512-Ua0sE9pgNoGrKXJ19ZhpZAnoJ3K3W/NweOb8GX5eFjQHcHPczaBlXBFslHHvlwhcW0uapNsiZmt/AVLZZtn1tA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/static-config": "3.3.0" } }, "node_modules/@vercel/nestjs": { - "version": "0.2.81", - "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.81.tgz", - "integrity": "sha512-cSmnhW3QJqiVhkGw/F7M15e3KVyhyRrdbVWi8apjDouFAw/RB+JI3osTFkh/DAs3weWbcHSJbnOg/4NQiiOxcg==", + "version": "0.2.82", + "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.82.tgz", + "integrity": "sha512-ddwGGnxtw3DNPmGB6z/8F2qqpqdT9+c2As64bQx/1JfL4vNbx2bPbUCvH4waxJO5H7FjNODpLDUbnQvI1nejdw==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/static-config": "3.3.0" } }, @@ -10531,16 +10495,16 @@ } }, "node_modules/@vercel/node": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.1.tgz", - "integrity": "sha512-/ql0AI9LDe1wK5x/iX2fV3h9jLdKyfTx9Y8NyL0UrdjK+S9SpJ4qyezES7LGYNTHge3DRlMP6sdYuUztOIdthw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.8.2.tgz", + "integrity": "sha512-Wt6KBr0LoIhUuzeH7E8S+1HRlS6oSA+98FJH/59wY2tYoxsHXbx5uiNkSBqBsM0nIwlJWR3B86tm9q9Xi22XdQ==", "license": "Apache-2.0", "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", - "@vercel/build-utils": "13.24.0", + "@vercel/build-utils": "13.25.0", "@vercel/error-utils": "2.1.0", "@vercel/nft": "1.5.0", "@vercel/static-config": "3.3.0", @@ -11114,9 +11078,9 @@ "license": "MIT" }, "node_modules/@vercel/python": { - "version": "6.41.0", - "resolved": "https://registry.npmjs.org/@vercel/python/-/python-6.41.0.tgz", - "integrity": "sha512-tLyaod+ZXcx2Wc7bJe9ubo2SuQoY4Fjl7M1NiconiR9DHBPB8kU1gTnisunGpy9cU4v0+KrOFORJA1FJSZTMVw==", + "version": "6.42.0", + "resolved": "https://registry.npmjs.org/@vercel/python/-/python-6.42.0.tgz", + "integrity": "sha512-zLcVpnP/EJ+7v8TRMoVuggflLhBDpeVh3vODJsz+mAMhxTOsI091Pjlc27oeBMD2Yp/zOhKX21Hte+ZwvUDuPg==", "license": "Apache-2.0", "dependencies": { "@vercel/python-analysis": "0.11.1" @@ -11340,13 +11304,13 @@ } }, "node_modules/@vercel/static-build": { - "version": "2.9.26", - "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.9.26.tgz", - "integrity": "sha512-xPSE7Du4S0EZrIA7xV40rd/mNR96lJNIMjf15g9cQrNBgYajaZ/DnXbJVQ7mlAfoo36XZFRF+YXTXvNa4qJ7VA==", + "version": "2.9.27", + "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.9.27.tgz", + "integrity": "sha512-rC0FuYv5cxnNOSg/1IXlYImP3k7xNy+xoh03AmM0zKDQZLxP4daeqLLGhXrSWeI7+YYmCO40az7vsfy/INhBcA==", "license": "Apache-2.0", "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", - "@vercel/gatsby-plugin-vercel-builder": "2.2.4", + "@vercel/gatsby-plugin-vercel-builder": "2.2.5", "@vercel/static-config": "3.3.0", "ts-morph": "12.0.0" } @@ -14199,9 +14163,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.29", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", - "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "version": "2.10.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", + "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -17594,15 +17558,15 @@ } }, "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", + "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", @@ -18587,9 +18551,9 @@ } }, "node_modules/focus-trap": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.0.tgz", - "integrity": "sha512-CaBdQ9P4fa/yCA6pDf/3aJd8bf9IOG5QGK21/E+86o2V4V8kzXaR4A9E6tNR7KkkS1+T5ZIU1tJDBDLwsucz9g==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.1.tgz", + "integrity": "sha512-6CxwrrFRquH7pDXb1mWxudkU9LSfYBMRZutpgddb2o6iwCk7cIRrBhyY3c8SGKcmIKdeMTrGSNg4Bedh2RSF/w==", "license": "MIT", "dependencies": { "tabbable": "^6.4.0" @@ -21443,9 +21407,9 @@ "license": "ISC" }, "node_modules/isomorphic-git": { - "version": "1.37.6", - "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.37.6.tgz", - "integrity": "sha512-qr1NFCPsVTZ6YGqTXw0CzamnsHyH9QQ1OTEfeXIweSljRUMzuHFCJdUn0wc6OcjtTDns6knxjPb7N6LmJeftOA==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.38.0.tgz", + "integrity": "sha512-gsBFnAT8Fxrpx+53ymG5kEOHSrUDVcSMFl7fCEGVnPpQbPS0aKti3UzZXR+3DKA0yyf+4z6CXJxULlQ5QPxDJw==", "license": "MIT", "dependencies": { "async-lock": "^1.4.1", @@ -21665,12 +21629,12 @@ } }, "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.7.tgz", + "integrity": "sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/js-library-detector": { @@ -22143,9 +22107,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.1.tgz", - "integrity": "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.2.tgz", + "integrity": "sha512-S3kmBrptp3yRTm83NUcHy9g1vbwiWMzI8WvY22+koBJ6zkRteLnedBL2VX0MIAGwx2yiyxX4J85pceZyQ6ffgg==", "license": "MIT" }, "node_modules/libsql": { @@ -30820,9 +30784,9 @@ } }, "node_modules/preact": { - "version": "10.29.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", - "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -31126,24 +31090,24 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.0.2.tgz", + "integrity": "sha512-cXj/5RlDCzSC7k1YdBIm6prb8lK8lEdmScVbcalX1rBn4fqNN1UNuEz/HZZYiDLsK8dOGvyLpGjh6CgxCyqKtg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", + "@puppeteer/browsers": "3.0.2", + "chromium-bidi": "16.0.1", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "puppeteer-core": "25.0.2", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { @@ -31170,12 +31134,94 @@ "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", "license": "BSD-3-Clause" }, + "node_modules/puppeteer/node_modules/@puppeteer/browsers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.2.tgz", + "integrity": "sha512-JnOSHrAdCQOj27P5QnTrd6bkYd9cXXeFMJS5UJF3UmQbpZQAMMO7AaL0NyrT7i2l/43bwjaHguU+LOpBRyx66w==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "progress": "^2.0.3", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/puppeteer/node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, "node_modules/puppeteer/node_modules/devtools-protocol": { "version": "0.0.1608973", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", "license": "BSD-3-Clause" }, + "node_modules/puppeteer/node_modules/puppeteer-core": { + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.0.2.tgz", + "integrity": "sha512-Q0IUIHER1S9PiNIfdNFc+pVOj79Tp4b9v0Fv4enigwsLy0Hbgq45KFgqzmN31DeCXh+Uvxnt9r7fMERhAMjs8Q==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.2", + "chromium-bidi": "16.0.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/puppeteer/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/qified": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz", @@ -38081,35 +38127,35 @@ } }, "node_modules/vercel": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/vercel/-/vercel-54.0.0.tgz", - "integrity": "sha512-+uDvXt7x0W0GnOt0op5IP2iN9gm2+x5OZDC7HfdOSffJx2xcchNrWWvINqnilhFwpopy1qCcbFmFSyfRXskhAA==", + "version": "54.1.0", + "resolved": "https://registry.npmjs.org/vercel/-/vercel-54.1.0.tgz", + "integrity": "sha512-7Gx3KCe+yPmZ6aN9RbxovP3ipyO0BFDV5ckxUwaMiNFGt0BYb4TYRQm/JosaC/Jb3hHexYcJsiyoLVsQ5Ow00w==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.6.0", + "@vercel/backends": "0.7.0", "@vercel/blob": "2.3.0", - "@vercel/build-utils": "13.24.0", + "@vercel/build-utils": "13.25.0", "@vercel/cli-config": "0.1.1", "@vercel/detect-agent": "1.2.3", - "@vercel/elysia": "0.1.77", - "@vercel/express": "0.1.87", - "@vercel/fastify": "0.1.80", + "@vercel/elysia": "0.1.78", + "@vercel/express": "0.1.88", + "@vercel/fastify": "0.1.81", "@vercel/fun": "1.3.0", - "@vercel/go": "3.6.0", - "@vercel/h3": "0.1.86", - "@vercel/hono": "0.2.80", + "@vercel/go": "3.7.0", + "@vercel/h3": "0.1.87", + "@vercel/hono": "0.2.81", "@vercel/hydrogen": "1.3.7", - "@vercel/koa": "0.1.60", - "@vercel/nestjs": "0.2.81", + "@vercel/koa": "0.1.61", + "@vercel/nestjs": "0.2.82", "@vercel/next": "4.17.1", - "@vercel/node": "5.8.1", + "@vercel/node": "5.8.2", "@vercel/prepare-flags-definitions": "0.2.1", - "@vercel/python": "6.41.0", + "@vercel/python": "6.42.0", "@vercel/redwood": "2.4.13", "@vercel/remix-builder": "5.8.1", "@vercel/ruby": "2.3.2", "@vercel/rust": "1.2.0", - "@vercel/static-build": "2.9.26", + "@vercel/static-build": "2.9.27", "chokidar": "4.0.0", "esbuild": "0.27.0", "form-data": "^4.0.0", diff --git a/package.json b/package.json index 3619f8b3..68723e1e 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "astro": "^6.3.3", "astro-og-canvas": "0.11.1", "astro-vtbot": "2.1.12", - "baseline-browser-mapping": "2.10.29", + "baseline-browser-mapping": "2.10.30", "canvas-confetti": "1.9.4", "confusing-browser-globals": "1.0.11", "cross-env": "10.1.0", @@ -154,13 +154,13 @@ "email-validator": "2.0.4", "embla-carousel": "8.6.0", "embla-carousel-autoplay": "8.6.0", - "eslint": "10.3.0", + "eslint": "10.4.0", "eslint-plugin-astro": "1.7.0", "eslint-plugin-import-lite": "0.6.0", "eslint-plugin-jsdoc": "62.9.0", "eslint-plugin-security": "4.0.0", "eslint-plugin-yml": "3.3.2", - "focus-trap": "8.2.0", + "focus-trap": "8.2.1", "globals": "17.6.0", "gsap": "3.15.0", "happy-dom": "20.9.0", @@ -171,10 +171,10 @@ "husky": "9.1.7", "install": "0.13.0", "is-whitespace-character": "2.0.1", - "isomorphic-git": "1.37.6", - "js-cookie": "3.0.5", + "isomorphic-git": "1.38.0", + "js-cookie": "3.0.7", "jsdom": "29.1.1", - "libphonenumber-js": "1.13.1", + "libphonenumber-js": "1.13.2", "lighthouse": "13.3.0", "lit": "3.3.3", "markdownlint-cli2": "0.22.1", @@ -190,11 +190,11 @@ "playwright-lighthouse": "4.0.0", "postcss": "8.5.14", "postcss-html": "1.8.1", - "preact": "10.29.1", + "preact": "10.29.2", "prettier": "3.8.3", "prettier-plugin-astro": "0.14.1", "pubsub-js": "1.9.5", - "puppeteer": "24.43.1", + "puppeteer": "25.0.2", "qr-code-styling": "1.9.2", "rehype-accessible-emojis": "0.3.2", "rehype-external-links": "3.0.0", @@ -246,7 +246,7 @@ "unist-util-is": "6.0.1", "unist-util-visit": "5.1.0", "uuid": "14.0.0", - "vercel": "54.0.0", + "vercel": "54.1.0", "vite": "7.3.2", "vite-plugin-pwa": "1.3.0", "vitest": "4.1.6", diff --git a/src/components/Content/Layout/index.astro b/src/components/Content/Layout/index.astro index 296f7337..e7e95c58 100644 --- a/src/components/Content/Layout/index.astro +++ b/src/components/Content/Layout/index.astro @@ -19,6 +19,8 @@ export type Props = { author: string /** Content collection entry */ article: CollectionEntry<'articles'> | CollectionEntry<'deepDives'> + /** Canonical URL path when it differs from the rendered page path */ + canonicalPath?: string /** Canonical URL path, e.g. '/articles/my-slug' */ path: string /** Estimated reading time label */ @@ -27,11 +29,12 @@ export type Props = { section: string } -const { author, article, path, readingTime, section } = Astro.props +const { author, article, canonicalPath, path, readingTime, section } = Astro.props --- - + {/* Sitemap */} diff --git a/src/components/Head/StructuredData.astro b/src/components/Head/StructuredData.astro index 311d267c..4e826f27 100644 --- a/src/components/Head/StructuredData.astro +++ b/src/components/Head/StructuredData.astro @@ -3,8 +3,16 @@ import { getSchemas } from '@components/Head/server/structuredData' import { applyRenderSentryContext } from '@lib/sentry/renderContext' export type { StructuredDataProps as Props } from '@components/Head/server/structuredData' -const { path, pageTitle, pageDescription, contentType, publishDate, modifiedDate, author } = - Astro.props +const { + path, + canonicalPath, + pageTitle, + pageDescription, + contentType, + publishDate, + modifiedDate, + author, +} = Astro.props applyRenderSentryContext({ contextName: 'structuredDataRender', @@ -30,6 +38,7 @@ applyRenderSentryContext({ const schemas = getSchemas({ astro: Astro, path, + ...(canonicalPath && { canonicalPath }), pageTitle, ...(pageDescription && { pageDescription }), ...(contentType && { contentType }), diff --git a/src/components/Head/__tests__/Meta.spec.ts b/src/components/Head/__tests__/Meta.spec.ts new file mode 100644 index 00000000..c283fd97 --- /dev/null +++ b/src/components/Head/__tests__/Meta.spec.ts @@ -0,0 +1,33 @@ +import { beforeEach, describe, expect, test } from 'vitest' +import { experimental_AstroContainer as AstroContainer } from 'astro/container' +import { JSDOM } from 'jsdom' + +describe('Meta (Astro)', () => { + let container: AstroContainer + + beforeEach(async () => { + container = await AstroContainer.create() + }) + + test('renders canonical link from canonicalPath when provided', async () => { + const Meta = (await import('@components/Head/Meta.astro')).default + + const response = await container.renderToResponse(Meta, { + props: { + pageTitle: 'Example Article', + pageDescription: 'Example description', + path: '/articles/example-article', + canonicalPath: '/deep-dive/example-article', + }, + request: new Request('https://example.com/articles/example-article'), + partial: false, + }) + + const renderedHtml = await response.text() + const document = new JSDOM(renderedHtml).window.document + + expect(document.querySelector('link[rel="canonical"]')?.getAttribute('href')).toBe( + 'https://example.com/deep-dive/example-article' + ) + }) +}) \ No newline at end of file diff --git a/src/components/Head/index.astro b/src/components/Head/index.astro index 326b3abd..5f7fcf73 100644 --- a/src/components/Head/index.astro +++ b/src/components/Head/index.astro @@ -5,6 +5,8 @@ import ThemeInit from '@components/Head/ThemeInit.astro' export interface Props { /** Author name */ author?: string + /** Canonical URL path when it differs from the rendered page path */ + canonicalPath?: string /** Content type for OpenGraph metadata */ contentType?: 'article' | 'website' /** Meta description */ @@ -27,6 +29,7 @@ export interface Props { const { author, + canonicalPath, contentType, pageDescription, modifiedDate, @@ -48,6 +51,7 @@ const { { expect(contactPage?.url).toBe('https://www.webstackbuilders.com/contact') }) + it('uses canonicalPath for article schema urls while keeping breadcrumbs on the rendered path', () => { + const schemas = getSchemas( + createStructuredDataParams({ + path: '/articles/example-article', + canonicalPath: '/deep-dive/example-article', + contentType: 'article', + publishDate: new Date('2026-01-01T00:00:00.000Z'), + }) + ) + + const parsed = schemas.map(schema => JSON.parse(schema)) + const article = parsed.find(schema => schema['@type'] === 'Article') + const breadcrumbs = parsed.find(schema => schema['@type'] === 'BreadcrumbList') + + expect(article?.url).toBe('https://www.webstackbuilders.com/deep-dive/example-article') + expect(breadcrumbs?.itemListElement?.[1]?.item).toBe( + 'https://www.webstackbuilders.com/articles' + ) + }) + it('wraps serialization failures in a BuildError', () => { const stringifySpy = vi.spyOn(JSON, 'stringify').mockImplementationOnce(() => { throw new TypeError('circular structure') diff --git a/src/components/Head/server/structuredData.ts b/src/components/Head/server/structuredData.ts index 8761029b..0e69c46d 100644 --- a/src/components/Head/server/structuredData.ts +++ b/src/components/Head/server/structuredData.ts @@ -21,6 +21,7 @@ const ICON_PATH = '/icon-512.png' type ContentType = 'article' | 'website' export interface StructuredDataProps { + canonicalPath?: string path: string pageTitle: string pageDescription?: string @@ -66,6 +67,7 @@ export const getSchemas = (params: StructuredDataParams): string[] => { const createSchemaContext = (params: StructuredDataParams): SchemaContext => { const { astro, + canonicalPath, path, pageTitle, pageDescription, @@ -95,7 +97,8 @@ const createSchemaContext = (params: StructuredDataParams): SchemaContext => { const site = resolveSiteUrl(astro) const normalizedPath = normalizePath(path) - const canonicalUrl = astro.url?.href ?? resolveRoute(normalizedPath, site) + const normalizedCanonicalPath = normalizePath(canonicalPath ?? normalizedPath) + const canonicalUrl = resolveRoute(normalizedCanonicalPath, site) const pageDescriptionFallback = pageDescription ?? companyContactData.description const socialImageUrl = getSocialImageLink(path, site) diff --git a/src/components/Navigation/client/__tests__/index.spec.ts b/src/components/Navigation/client/__tests__/index.spec.ts index ac2fea69..4e0cc28b 100644 --- a/src/components/Navigation/client/__tests__/index.spec.ts +++ b/src/components/Navigation/client/__tests__/index.spec.ts @@ -12,12 +12,17 @@ import type { WebComponentModule } from '@components/scripts/@types/webComponent import { executeRender } from '@test/unit/helpers/litRuntime' const setOverlayPauseStateMock = vi.fn() +const handleScriptErrorMock = vi.fn() vi.mock('@components/scripts/store', () => ({ setOverlayPauseState: setOverlayPauseStateMock, })) -const navigateMock = vi.fn() +vi.mock('@components/scripts/errors/handler', () => ({ + handleScriptError: handleScriptErrorMock, +})) + +const navigateMock = vi.fn(() => Promise.resolve()) const focusTrapMock = { activate: vi.fn(), deactivate: vi.fn(), @@ -49,6 +54,8 @@ describe('NavigationElement web component behavior', () => { beforeEach(async () => { container = await AstroContainer.create() navigateMock.mockClear() + navigateMock.mockImplementation(() => Promise.resolve()) + handleScriptErrorMock.mockClear() focusTrapMock.activate.mockClear() focusTrapMock.deactivate.mockClear() setOverlayPauseStateMock.mockClear() @@ -138,6 +145,46 @@ describe('NavigationElement web component behavior', () => { }) }) + test('aborted view transition navigation rejections are ignored', async () => { + navigateMock.mockRejectedValueOnce( + Object.assign(new Error('Transition was aborted because of invalid state'), { + name: 'InvalidStateError', + }) + ) + + await renderNavigation(async () => { + const firstLink = document.querySelector('.main-nav-menu a') + if (!isAnchorElement(firstLink)) { + throw new TestError('Navigation link not found') + } + + firstLink.click() + await Promise.resolve() + + expect(handleScriptErrorMock).not.toHaveBeenCalled() + }) + }) + + test('unexpected navigation rejections are reported', async () => { + const navigationError = new Error('Navigation failed') + navigateMock.mockRejectedValueOnce(navigationError) + + await renderNavigation(async () => { + const firstLink = document.querySelector('.main-nav-menu a') + if (!isAnchorElement(firstLink)) { + throw new TestError('Navigation link not found') + } + + firstLink.click() + await Promise.resolve() + + expect(handleScriptErrorMock).toHaveBeenCalledWith(navigationError, { + scriptName: 'NavigationElement', + operation: 'navigate', + }) + }) + }) + test('focus trap activate/deactivate when menu toggles', async () => { await renderNavigation(async () => { const toggleButton = document.querySelector('.nav-toggle-btn') diff --git a/src/components/Navigation/client/index.ts b/src/components/Navigation/client/index.ts index 62304997..44ef4d2e 100644 --- a/src/components/Navigation/client/index.ts +++ b/src/components/Navigation/client/index.ts @@ -35,6 +35,20 @@ export const ATTRIBUTES = { navOpen: 'data-nav-open', } +const ABORTED_VIEW_TRANSITION_MESSAGE = 'Transition was aborted because of invalid state' + +const isAbortedViewTransitionError = (error: unknown): boolean => { + if (!(error instanceof Error)) { + return false + } + + return ( + error.name === 'InvalidStateError' && + typeof error.message === 'string' && + error.message.includes(ABORTED_VIEW_TRANSITION_MESSAGE) + ) +} + export class NavigationElement extends LitElement { static registeredName = 'site-navigation' @@ -201,7 +215,16 @@ export class NavigationElement extends LitElement { this.toggleMenu(false) } - navigate(href) + void navigate(href).catch(error => { + if (isAbortedViewTransitionError(error)) { + return + } + + handleScriptError(error, { + scriptName: SCRIPT_NAME, + operation: 'navigate', + }) + }) }) } catch (error) { handleScriptError(error, { diff --git a/src/components/scripts/sentry/__tests__/helpers.spec.ts b/src/components/scripts/sentry/__tests__/helpers.spec.ts index 5c4bdc41..441c6ca1 100644 --- a/src/components/scripts/sentry/__tests__/helpers.spec.ts +++ b/src/components/scripts/sentry/__tests__/helpers.spec.ts @@ -250,6 +250,24 @@ const createConsentCheckpointClientErrorEvent = (): Parameters[0] +const createAbortedViewTransitionErrorEvent = (): Parameters[0] => + ({ + type: 'error', + message: 'InvalidStateError: Transition was aborted because of invalid state', + exception: { + values: [ + { + type: 'DOMException', + value: 'InvalidStateError: Transition was aborted because of invalid state', + mechanism: { + type: 'auto.browser.global_handlers.onunhandledrejection', + handled: false, + }, + }, + ], + }, + }) as unknown as Parameters[0] + const createHint = (): Parameters[1] => ({}) as Parameters[1] @@ -420,6 +438,17 @@ describe('sentry helpers', () => { expect(result).toBeNull() }) + it('drops aborted Astro view transition rejection noise', () => { + isProdMock.mockReturnValue(true) + getConsentSnapshotMock.mockReturnValue({ analytics: true }) + + const event = createAbortedViewTransitionErrorEvent() + + const result = beforeSendHandler(event, createHint()) + + expect(result).toBeNull() + }) + it('scrubs PII when analytics consent is missing and preserves safe breadcrumbs', () => { isProdMock.mockReturnValue(true) getConsentSnapshotMock.mockReturnValue({ analytics: false }) diff --git a/src/components/scripts/sentry/helpers.ts b/src/components/scripts/sentry/helpers.ts index 8a3bd065..c245cef7 100644 --- a/src/components/scripts/sentry/helpers.ts +++ b/src/components/scripts/sentry/helpers.ts @@ -4,6 +4,8 @@ import { getConsentSnapshot } from '@components/scripts/store/consent' type BeforeSendHandler = NonNullable +const ABORTED_VIEW_TRANSITION_MESSAGE = 'Transition was aborted because of invalid state' + const SAFE_BREADCRUMB_CATEGORIES = new Set(['script', 'sentry.event']) const isConsentActionRequest = (requestUrl: string): boolean => { @@ -196,6 +198,22 @@ const isHandledConsentCheckpointClientError = ( ) } +const isHandledAbortedViewTransitionError = ( + event: Parameters[0] +): boolean => { + const exception = event.exception?.values?.[0] + const mechanismType = exception?.mechanism?.type + const errorType = exception?.type + const errorMessage = exception?.value ?? event.message ?? '' + + return ( + mechanismType === 'auto.browser.global_handlers.onunhandledrejection' && + errorType === 'DOMException' && + typeof errorMessage === 'string' && + errorMessage.includes(ABORTED_VIEW_TRANSITION_MESSAGE) + ) +} + function scrubBreadcrumbs( breadcrumbs: NonNullable[0]['breadcrumbs']> ) { @@ -282,6 +300,13 @@ export const beforeSendHandler: BeforeSendHandler = (event, _hint) => { return null } + // Astro view transitions can abort an in-flight navigation when a newer + // navigation supersedes it. The browser surfaces that as an unhandled + // DOMException rejection with no user-visible failure, so drop it as noise. + if (isHandledAbortedViewTransitionError(event)) { + return null + } + const currentConsent = getConsentSnapshot() if (!currentConsent.analytics) { if (event.user) { diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index e786ee3c..fb44b094 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -25,6 +25,8 @@ export interface Props { author?: string /** Optional title override for the breadcrumbs UI */ breadcrumbTitle?: string + /** Canonical URL path when it differs from the rendered page path */ + canonicalPath?: string /** Hide breadcrumbs on mobile viewports (sm and below) */ hideBreadcrumbsOnMobile?: boolean /** Enforce that child is passed to slot */ @@ -57,6 +59,7 @@ export interface Props { const { author, breadcrumbTitle, + canonicalPath, contentType, pageDescription, hideBreadcrumbsOnMobile = false, @@ -118,6 +121,7 @@ applyRenderSentryContext({ @@ -115,6 +117,7 @@ export interface Props { const { author, + canonicalPath, collectionItem, contentClass = '', contentType = 'article', @@ -145,6 +148,7 @@ const link = `https://www.webstackbuilders.com${path}` { describe('Social Card API - GET /api/social-card', () => { const expectedAvatarPath = fileURLToPath( - new URL('../../../../assets/images/avatars/kevin-brown.webp', import.meta.url) + new URL('../../../../../public/assets/images/kevin-brown.webp', import.meta.url) ) beforeEach(() => { diff --git a/src/pages/api/social-card/index.ts b/src/pages/api/social-card/index.ts index cc63069b..78921e79 100644 --- a/src/pages/api/social-card/index.ts +++ b/src/pages/api/social-card/index.ts @@ -1,6 +1,7 @@ import type { APIRoute } from 'astro' import { getCollection } from 'astro:content' import { generateOpenGraphImage } from 'astro-og-canvas' +import { existsSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { buildApiErrorResponse, handleApiFunctionError } from '@pages/api/_utils/errors' import { createApiFunctionContext } from '@pages/api/_utils/requestContext' @@ -8,9 +9,28 @@ import { createApiFunctionContext } from '@pages/api/_utils/requestContext' export const prerender = false const ROUTE = '/api/social-card' -const AVATAR_FILE_PATH = fileURLToPath( - new URL('../../../assets/images/avatars/kevin-brown.webp', import.meta.url) -) + +const resolveAvatarFilePath = (): string => { + const candidateUrls = [ + // Vercel serverless output after build. + new URL('../../static/assets/images/kevin-brown.webp', import.meta.url), + // Local source path for dev and test runs. + new URL('../../../../public/assets/images/kevin-brown.webp', import.meta.url), + // Legacy source asset path kept as a final fallback. + new URL('../../../assets/images/avatars/kevin-brown.webp', import.meta.url), + ] + + for (const candidateUrl of candidateUrls) { + const filePath = fileURLToPath(candidateUrl) + if (existsSync(filePath)) { + return filePath + } + } + + throw new Error('Social card avatar asset could not be resolved for the current runtime') +} + +const AVATAR_FILE_PATH = resolveAvatarFilePath() const DEFAULT_TITLE = 'Platform Engineering by Kevin Brown' const DEFAULT_DESCRIPTION = 'Platform engineer helping teams harden delivery, modernize cloud platforms, and improve developer experience.' diff --git a/src/pages/articles/[...slug].astro b/src/pages/articles/[...slug].astro index 147a3810..0a458c7d 100644 --- a/src/pages/articles/[...slug].astro +++ b/src/pages/articles/[...slug].astro @@ -8,6 +8,7 @@ import ContentLayout from '@components/Content/Layout/index.astro' export interface Props { article: CollectionEntry<'articles'> + canonicalPath?: string } /** Generate static paths for all articles */ @@ -15,13 +16,23 @@ export async function getStaticPaths() { const articleEntries = await getCollection('articles', ({ data }) => { return isDev() || data.isDraft !== true }) + + const deepDiveEntries = await getCollection('deepDives', ({ data }) => { + return isDev() || data.isDraft !== true + }) + + const deepDiveIds = new Set(deepDiveEntries.map(entry => entry.id)) + return articleEntries.map(article => ({ params: { slug: article.id }, - props: { article }, + props: { + article, + canonicalPath: deepDiveIds.has(article.id) ? `/deep-dive/${article.id}` : undefined, + }, })) } -const { article } = Astro.props +const { article, canonicalPath } = Astro.props const author = await getEntry(article.data.author) const articleBody = article.body ?? '' @@ -31,6 +42,7 @@ const readingTime = article.data.readingTime ?? getReadingTimeLabel(articleBody) `\ User-agent: * Allow: / +Disallow: /downloads/ +Disallow: /articles$ +Disallow: /case-studies$ +Disallow: /deep-dive$ +Disallow: /offline +Disallow: /print +Disallow: /search +Disallow: /tags +Disallow: /testing Sitemap: ${sitemapURL.href} `