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}
`