From 70b449ddbae465348aa9337ff7d1d62ad8286e1a Mon Sep 17 00:00:00 2001
From: Miodrag Obradovic <99922455+Kjubikstronk@users.noreply.github.com>
Date: Wed, 19 Aug 2026 11:30:41 +0200
Subject: [PATCH 1/8] fix(routing): keep the trailing slash when stripping an
injected .html (#17742)
With build.format: 'preserve' the build requests pages as /page.html.
Stripping that suffix left the pathname without the trailing slash that route
patterns are compiled with under trailingSlash: 'always', so a route stopped
matching itself, its params resolved as empty, and stringifyParams() threw
Missing parameter for any dynamic route.
---
.changeset/preserve-trailing-slash-html.md | 5 ++++
packages/astro/src/core/fetch/fetch-state.ts | 10 ++++++++
packages/astro/test/units/fetch/index.test.ts | 25 +++++++++++++++++++
3 files changed, 40 insertions(+)
create mode 100644 .changeset/preserve-trailing-slash-html.md
diff --git a/.changeset/preserve-trailing-slash-html.md b/.changeset/preserve-trailing-slash-html.md
new file mode 100644
index 000000000000..ea4023c077d4
--- /dev/null
+++ b/.changeset/preserve-trailing-slash-html.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixes `astro build` throwing `TypeError: Missing parameter` for dynamic routes when `build.format: 'preserve'` and `trailingSlash: 'always'` are used together. Stripping the framework-injected `.html` suffix dropped the trailing slash that the compiled route pattern requires, so the route no longer matched itself and its params resolved as empty.
diff --git a/packages/astro/src/core/fetch/fetch-state.ts b/packages/astro/src/core/fetch/fetch-state.ts
index 7bbe351383c0..f4325c2fa20b 100644
--- a/packages/astro/src/core/fetch/fetch-state.ts
+++ b/packages/astro/src/core/fetch/fetch-state.ts
@@ -959,6 +959,16 @@ export class FetchState implements AstroFetchState {
!routeHasHtmlExtension(this.routeData)
) {
this.pathname = this.pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '');
+ // Route patterns are compiled with the configured trailing slash, so a
+ // pathname left without one after stripping `.html` no longer matches its
+ // own route and yields no params.
+ if (
+ this.manifest.trailingSlash === 'always' &&
+ this.pathname !== '' &&
+ !this.pathname.endsWith('/')
+ ) {
+ this.pathname += '/';
+ }
}
}
diff --git a/packages/astro/test/units/fetch/index.test.ts b/packages/astro/test/units/fetch/index.test.ts
index 99f94dc95783..2765649040f2 100644
--- a/packages/astro/test/units/fetch/index.test.ts
+++ b/packages/astro/test/units/fetch/index.test.ts
@@ -125,6 +125,31 @@ describe('FetchState (astro/fetch)', () => {
assert.equal(state.routeData!.type, 'endpoint');
assert.equal(state.pathname, '/file.html', '.html should be preserved for endpoint routes');
});
+
+ it('restores the trailing slash after stripping .html when trailingSlash is always', () => {
+ // Regression test for #17726: with `build.format: 'preserve'` the build
+ // requests `/welcome.html`, and route patterns are compiled with the
+ // configured trailing slash. Stripping `.html` without restoring the `/`
+ // leaves a pathname that no longer matches its own route, so the params
+ // come back empty and `stringifyParams()` throws `Missing parameter`.
+ const app = createTestApp([createPage(simplePage, { route: '/welcome' })], {
+ trailingSlash: 'always',
+ });
+ const request = stampApp(new Request('http://example.com/welcome.html'), app);
+ const state = new FetchState(request);
+
+ assert.equal(state.pathname, '/welcome/', 'trailing slash should survive .html stripping');
+ });
+
+ it('does not add a trailing slash when trailingSlash is not always', () => {
+ const app = createTestApp([createPage(simplePage, { route: '/welcome' })], {
+ trailingSlash: 'ignore',
+ });
+ const request = stampApp(new Request('http://example.com/welcome.html'), app);
+ const state = new FetchState(request);
+
+ assert.equal(state.pathname, '/welcome', 'pathname should be left without a trailing slash');
+ });
});
// #endregion
From bc171af0e29a1bb4ca56beffde1c4c03e1bb227f Mon Sep 17 00:00:00 2001
From: Erika <3019731+Princesseuh@users.noreply.github.com>
Date: Wed, 19 Aug 2026 11:52:29 +0200
Subject: [PATCH 2/8] =?UTF-8?q?feat(satteri):=20update=20to=20S=C3=A4tteri?=
=?UTF-8?q?=200.10.x=20(#17731)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.changeset/satteri-0-10-3.md | 6 +
packages/integrations/mdx/package.json | 2 +-
.../remark/test/remark-collect-images.test.ts | 2 +-
packages/markdown/remark/test/shiki.test.ts | 2 +-
packages/markdown/satteri/package.json | 3 +-
.../markdown/satteri/src/satteri-processor.ts | 5 +-
pnpm-lock.yaml | 260 +++++++++---------
7 files changed, 145 insertions(+), 135 deletions(-)
create mode 100644 .changeset/satteri-0-10-3.md
diff --git a/.changeset/satteri-0-10-3.md b/.changeset/satteri-0-10-3.md
new file mode 100644
index 000000000000..77fe2bb4e1bd
--- /dev/null
+++ b/.changeset/satteri-0-10-3.md
@@ -0,0 +1,6 @@
+---
+'@astrojs/markdown-satteri': patch
+'@astrojs/mdx': patch
+---
+
+Updates Sätteri processor to v0.10.3. See [its changelog](https://github.com/bruits/satteri/blob/main/packages/satteri/CHANGELOG.md#0103--2026-08-19) for details on bugs fixed and features added.
diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json
index 55395b99b891..4713e5c3d9bd 100644
--- a/packages/integrations/mdx/package.json
+++ b/packages/integrations/mdx/package.json
@@ -76,7 +76,7 @@
"remark-math": "^6.0.0",
"remark-rehype": "^11.1.2",
"remark-toc": "^9.0.0",
- "satteri": "^0.9.1",
+ "satteri": "^0.10.3",
"shiki": "^4.0.2",
"unified": "^11.0.5",
"vite": "^8.0.13"
diff --git a/packages/markdown/remark/test/remark-collect-images.test.ts b/packages/markdown/remark/test/remark-collect-images.test.ts
index 6ca368996018..d337d3c66489 100644
--- a/packages/markdown/remark/test/remark-collect-images.test.ts
+++ b/packages/markdown/remark/test/remark-collect-images.test.ts
@@ -18,7 +18,7 @@ describe('collect images', async () => {
visit(tree, 'element', (node) => {
if (node.tagName === 'img') {
node.properties.className = ['image-class'];
- node.properties.htmlFor = 'some-id';
+ node.properties.htmlFor = ['some-id'];
}
});
};
diff --git a/packages/markdown/remark/test/shiki.test.ts b/packages/markdown/remark/test/shiki.test.ts
index 51c2048be196..7fa8ff00ad09 100644
--- a/packages/markdown/remark/test/shiki.test.ts
+++ b/packages/markdown/remark/test/shiki.test.ts
@@ -51,7 +51,7 @@ describe('shiki syntax highlighting', () => {
const root = hast.children[0] as Element;
assert.match(root.properties.class as string, /astro-code github-dark/);
- assert.match(root.properties.style as string, /background-color:#24292e;color:#e1e4e8;/);
+ assert.match(root.properties.style!, /background-color:#24292e;color:#e1e4e8;/);
});
it('createShikiHighlighter can reuse the same instance for different languages', async () => {
diff --git a/packages/markdown/satteri/package.json b/packages/markdown/satteri/package.json
index c5bdb84daeeb..133ea380c5f2 100644
--- a/packages/markdown/satteri/package.json
+++ b/packages/markdown/satteri/package.json
@@ -29,8 +29,7 @@
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/prism": "workspace:*",
"github-slugger": "^2.0.0",
- "hast-util-from-html": "^2.0.3",
- "satteri": "^0.9.1"
+ "satteri": "^0.10.3"
},
"devDependencies": {
"astro-scripts": "workspace:*"
diff --git a/packages/markdown/satteri/src/satteri-processor.ts b/packages/markdown/satteri/src/satteri-processor.ts
index 648ede2443fa..6cbacf4e3f4a 100644
--- a/packages/markdown/satteri/src/satteri-processor.ts
+++ b/packages/markdown/satteri/src/satteri-processor.ts
@@ -249,11 +249,12 @@ export async function createHighlightFn(
if (syntaxHighlightType === 'prism') {
const { runHighlighterWithAstro } = await import('@astrojs/prism/dist/highlighter');
- const { fromHtml } = await import('hast-util-from-html');
+ const { htmlToHast } = await loadSatteri();
return async (code, lang) => {
const { html, classLanguage } = await runHighlighterWithAstro(lang, code);
const pre = `
${html}
`;
- return fromHtml(pre, { fragment: true }).children[0] as HastNode;
+ const tree = htmlToHast(pre, { fragment: true });
+ return ('children' in tree ? tree.children[0] : undefined) as HastNode;
};
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7bf5bdab9bfa..1756ab5bfee6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -248,7 +248,7 @@ importers:
version: 3.1.0(eslint@10.4.0)
knip:
specifier: 5.82.1
- version: 5.82.1(@types/node@22.19.19)(typescript@6.0.3)
+ version: 5.82.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@22.19.19)(typescript@6.0.3)
pkg-pr-new:
specifier: ^0.0.71
version: 0.0.71
@@ -726,7 +726,7 @@ importers:
dependencies:
'@astrojs/compiler-rs':
specifier: ^0.3.2
- version: 0.3.2
+ version: 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
'@astrojs/internal-helpers':
specifier: workspace:*
version: link:../internal-helpers
@@ -5400,7 +5400,7 @@ importers:
version: 1.0.8
'@types/hast':
specifier: ^3.0.4
- version: 3.0.4
+ version: 3.0.5
'@types/mdast':
specifier: ^4.0.4
version: 4.0.4
@@ -5438,8 +5438,8 @@ importers:
specifier: ^9.0.0
version: 9.0.0
satteri:
- specifier: ^0.9.1
- version: 0.9.1
+ specifier: ^0.10.3
+ version: 0.10.3
shiki:
specifier: ^4.0.2
version: 4.0.2
@@ -6665,7 +6665,7 @@ importers:
dependencies:
'@types/hast':
specifier: ^3.0.4
- version: 3.0.4
+ version: 3.0.5
'@types/mdast':
specifier: ^4.0.4
version: 4.0.4
@@ -7036,7 +7036,7 @@ importers:
version: 1.0.8
'@types/hast':
specifier: ^3.0.4
- version: 3.0.4
+ version: 3.0.5
'@types/mdast':
specifier: ^4.0.4
version: 4.0.4
@@ -7067,12 +7067,9 @@ importers:
github-slugger:
specifier: ^2.0.0
version: 2.0.0
- hast-util-from-html:
- specifier: ^2.0.3
- version: 2.0.3
satteri:
- specifier: ^0.9.1
- version: 0.9.1
+ specifier: ^0.10.3
+ version: 0.10.3
devDependencies:
astro-scripts:
specifier: workspace:*
@@ -7687,29 +7684,52 @@ packages:
resolution: {integrity: sha512-6XG8TZt8DVYLuGDVSpFJaSMlNowOg5RGecvWbKvlgMoqVbztUAQ3AcWq6oZ5DoCnTzNAPcO7rhkwt8ZVgrS7CQ==}
engines: {node: '>=18'}
- '@bruits/satteri-darwin-arm64@0.9.1':
- resolution: {integrity: sha512-NE4qC2sRd0+R+oPMsKcUikIvWTGpAV16fSXNBoMSW7nK7Pd9e/ZGB7M8knep83pFmGmOb/5NbGKiVIh3oLbljw==}
+ '@bruits/satteri-darwin-arm64@0.10.3':
+ resolution: {integrity: sha512-RBPx54YSt6ly9ozJ4AbgY6ITep3uJAaPE0ErxhQnE9Mk5ldGeQYbq4bWiJyZOoQmqC45cMCZmFyPjQdJT4biEw==}
cpu: [arm64]
os: [darwin]
- '@bruits/satteri-darwin-x64@0.9.1':
- resolution: {integrity: sha512-Am8z5nX0L/sJR/n7np+5rMVP434MOBe3zlU+IO8fXbv2UaPNRCrEQPMS6lyxCj7dZHQI2AynSJw3v4fgQE8xSQ==}
+ '@bruits/satteri-darwin-x64@0.10.3':
+ resolution: {integrity: sha512-l0G6eARLHwlFEc7KOzEr0Z7YVEUzAQVSDZcevGOng0ycFvY+Eg7SNpNAc5/k0XXKEM+Q62A8wUaaL+d0W63x7g==}
cpu: [x64]
os: [darwin]
- '@bruits/satteri-linux-x64-gnu@0.9.1':
- resolution: {integrity: sha512-Bivw60+SIfmlaU9wEZ39HAcyPh1xU9TvOrz4KJgc+ziRStQs4EzYoWW4Eh9aSdiol+xV0vjEWYuA79aF3dr7kg==}
+ '@bruits/satteri-linux-arm64-gnu@0.10.3':
+ resolution: {integrity: sha512-U6Lv74zONfwnKbzPAylqp45isfUVJNQPnROlX1mWJ7TLCkJRKeYEFyNe5auZEynVY0ZrgyFe7mKE04dmsFqEhw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@bruits/satteri-linux-arm64-musl@0.10.3':
+ resolution: {integrity: sha512-m/dsv+OsXuQMc0Mmkz1KAO0U/TbY+qe4puyhf08ce7EW5I+czxa5n5Vpy6ZTYwFvA3TtrokwiBRDApzz+oPeww==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@bruits/satteri-linux-x64-gnu@0.10.3':
+ resolution: {integrity: sha512-Y+YYrs3U+UhxRf3vn9Fgjo3NtjnqYBsAEpTrZcwfKpaiSXKpQlr4v2H9Lp4jtlC4PNjPr02wkiNtiAgNlhkB8w==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@bruits/satteri-wasm32-wasi@0.9.1':
- resolution: {integrity: sha512-lEFk5ebh5SxRquA5RJisEoMoDmOiSnS10PGgXgXeVlWgF8KWKI9D3hSzVqNjEg/qKOjHcNdjIfyx/03Wrl6J3g==}
+ '@bruits/satteri-linux-x64-musl@0.10.3':
+ resolution: {integrity: sha512-Fqg47592hkRebzI5EMU9BYnbFz7m/Q4awJt062xr9lweaHTmlN6ij/Z9Dzyt4THsqRFm87DZFMcwrniQpeEbPA==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@bruits/satteri-wasm32-wasi@0.10.3':
+ resolution: {integrity: sha512-OHBxAJMt97QNQJ8JbtdzdH54dWV7OT0pxl1wGZyD4vRDUeJjm5CXgeC/t1VJzD9tQiwPCVY3/Hh/88SJpVOj7g==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
- '@bruits/satteri-win32-x64-msvc@0.9.1':
- resolution: {integrity: sha512-YBhm8yARopswAPeTih11BzMxWVQFD5CI9Yryp6HWepi6F/CeMycqxv18DXrj9U2fDDlbVGwT2IiEM2UPBjIPDQ==}
+ '@bruits/satteri-win32-arm64-msvc@0.10.3':
+ resolution: {integrity: sha512-4+fLJgFthvZQHdpb1nVXQXH1T6spXs5fYj71KTvTOhsSZ3jA1VkvneVtv7VinX7vgtMu6PZGm/X3P84ancCMkA==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@bruits/satteri-win32-x64-msvc@0.10.3':
+ resolution: {integrity: sha512-BdbaBcnKktrNbAGibVnZFIZKtl+xl3TKvgLDNZempM0d2C+2l9OU874AqdhkOkegWk/vdCS1Z+wUD6W5KYUT9w==}
cpu: [x64]
os: [win32]
@@ -8213,18 +8233,9 @@ packages:
'@emnapi/core@1.11.1':
resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
- '@emnapi/core@1.9.1':
- resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
-
'@emnapi/runtime@1.11.1':
resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
- '@emnapi/runtime@1.9.1':
- resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
-
- '@emnapi/wasi-threads@1.2.0':
- resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
-
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
@@ -9208,12 +9219,12 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
- '@napi-rs/wasm-runtime@1.2.0':
- resolution: {integrity: sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==}
+ '@napi-rs/wasm-runtime@1.2.3':
+ resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
peerDependencies:
- '@emnapi/core': ^2.0.0-alpha.3
- '@emnapi/runtime': ^2.0.0-alpha.3
+ '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
+ '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
'@netlify/ai@0.4.1':
resolution: {integrity: sha512-ETLtV/9taYrcGhszwO+BLFgFJJ2MCnJp8BwxfwV6Z/+z3SsaUG4ExC8x4xzNCdB2GPWxXrXkvR2LFNsPFSLcRA==}
@@ -10290,8 +10301,8 @@ packages:
'@types/express@5.0.6':
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
- '@types/hast@3.0.4':
- resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+ '@types/hast@3.0.5':
+ resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
'@types/html-escaper@3.0.4':
resolution: {integrity: sha512-UKSaMPMXXKnq1jDj74seVikfdq5pWvoXcIgOUbwYzHuAEGiv8/juom1i/MsWBF8boFSI0uHQCSZauzr5OYnnJA==}
@@ -14773,8 +14784,8 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
- satteri@0.9.1:
- resolution: {integrity: sha512-0oIBjwDxWvz8ePRSBxv8vEdZ0GI25/UcSq11y4651tJztUAmZlIdPjFx8luqBAM22g8YKVr5R17KaaZ2kIfLrw==}
+ satteri@0.10.3:
+ resolution: {integrity: sha512-LBDONBUtj48XanFpLcCDvyCjBruz5aj5MGwE9gR+/RTB3monyglmh9jpGXuYUDO4rvBhvK2bpbjTKeU9kkHtfg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
@@ -16267,9 +16278,9 @@ snapshots:
'@astrojs/compiler-binding-linux-x64-musl@0.3.2':
optional: true
- '@astrojs/compiler-binding-wasm32-wasi@0.3.2':
+ '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
- '@napi-rs/wasm-runtime': 1.2.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -16281,7 +16292,7 @@ snapshots:
'@astrojs/compiler-binding-win32-x64-msvc@0.3.2':
optional: true
- '@astrojs/compiler-binding@0.3.2':
+ '@astrojs/compiler-binding@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
optionalDependencies:
'@astrojs/compiler-binding-darwin-arm64': 0.3.2
'@astrojs/compiler-binding-darwin-x64': 0.3.2
@@ -16289,16 +16300,16 @@ snapshots:
'@astrojs/compiler-binding-linux-arm64-musl': 0.3.2
'@astrojs/compiler-binding-linux-x64-gnu': 0.3.2
'@astrojs/compiler-binding-linux-x64-musl': 0.3.2
- '@astrojs/compiler-binding-wasm32-wasi': 0.3.2
+ '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
'@astrojs/compiler-binding-win32-arm64-msvc': 0.3.2
'@astrojs/compiler-binding-win32-x64-msvc': 0.3.2
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
- '@astrojs/compiler-rs@0.3.2':
+ '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
- '@astrojs/compiler-binding': 0.3.2
+ '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -16921,23 +16932,35 @@ snapshots:
dependencies:
modern-tar: 0.7.7
- '@bruits/satteri-darwin-arm64@0.9.1':
+ '@bruits/satteri-darwin-arm64@0.10.3':
optional: true
- '@bruits/satteri-darwin-x64@0.9.1':
+ '@bruits/satteri-darwin-x64@0.10.3':
optional: true
- '@bruits/satteri-linux-x64-gnu@0.9.1':
+ '@bruits/satteri-linux-arm64-gnu@0.10.3':
optional: true
- '@bruits/satteri-wasm32-wasi@0.9.1':
+ '@bruits/satteri-linux-arm64-musl@0.10.3':
+ optional: true
+
+ '@bruits/satteri-linux-x64-gnu@0.10.3':
+ optional: true
+
+ '@bruits/satteri-linux-x64-musl@0.10.3':
+ optional: true
+
+ '@bruits/satteri-wasm32-wasi@0.10.3':
dependencies:
- '@emnapi/core': 1.9.1
- '@emnapi/runtime': 1.9.1
- '@napi-rs/wasm-runtime': 1.2.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
optional: true
- '@bruits/satteri-win32-x64-msvc@0.9.1':
+ '@bruits/satteri-win32-arm64-msvc@0.10.3':
+ optional: true
+
+ '@bruits/satteri-win32-x64-msvc@0.10.3':
optional: true
'@capsizecss/unpack@4.0.0':
@@ -17575,27 +17598,11 @@ snapshots:
tslib: 2.8.1
optional: true
- '@emnapi/core@1.9.1':
- dependencies:
- '@emnapi/wasi-threads': 1.2.0
- tslib: 2.8.1
- optional: true
-
'@emnapi/runtime@1.11.1':
dependencies:
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.9.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
- '@emnapi/wasi-threads@1.2.0':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@emnapi/wasi-threads@1.2.2':
dependencies:
tslib: 2.8.1
@@ -18258,7 +18265,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.8
'@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdx': 2.0.13
acorn: 8.17.0
collapse-white-space: 2.1.0
@@ -18326,20 +18333,13 @@ snapshots:
'@tybys/wasm-util': 0.10.3
optional: true
- '@napi-rs/wasm-runtime@1.2.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
'@emnapi/core': 1.11.1
'@emnapi/runtime': 1.11.1
'@tybys/wasm-util': 0.10.3
optional: true
- '@napi-rs/wasm-runtime@1.2.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)':
- dependencies:
- '@emnapi/core': 1.9.1
- '@emnapi/runtime': 1.9.1
- '@tybys/wasm-util': 0.10.3
- optional: true
-
'@netlify/ai@0.4.1':
dependencies:
'@netlify/api': 14.0.18
@@ -18853,9 +18853,9 @@ snapshots:
'@oxc-resolver/binding-openharmony-arm64@11.17.1':
optional: true
- '@oxc-resolver/binding-wasm32-wasi@11.17.1':
+ '@oxc-resolver/binding-wasm32-wasi@11.17.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
- '@napi-rs/wasm-runtime': 1.2.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -19077,7 +19077,7 @@ snapshots:
dependencies:
'@emnapi/core': 1.11.1
'@emnapi/runtime': 1.11.1
- '@napi-rs/wasm-runtime': 1.2.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.1.2':
@@ -19182,7 +19182,7 @@ snapshots:
'@shikijs/primitive': 4.0.2
'@shikijs/types': 4.0.2
'@shikijs/vscode-textmate': 10.0.2
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@4.0.2':
@@ -19204,12 +19204,12 @@ snapshots:
dependencies:
'@shikijs/types': 4.0.2
'@shikijs/vscode-textmate': 10.0.2
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@shikijs/rehype@4.0.2':
dependencies:
'@shikijs/types': 4.0.2
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-to-string: 3.0.1
shiki: 4.0.2
unified: 11.0.5
@@ -19231,7 +19231,7 @@ snapshots:
'@shikijs/types@4.0.2':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@shikijs/vscode-textmate@10.0.2': {}
@@ -19535,7 +19535,7 @@ snapshots:
'@types/express-serve-static-core': 5.1.1
'@types/serve-static': 2.2.0
- '@types/hast@3.0.4':
+ '@types/hast@3.0.5':
dependencies:
'@types/unist': 3.0.3
@@ -22154,7 +22154,7 @@ snapshots:
hast-util-from-html@2.0.3:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
devlop: 1.1.0
hast-util-from-parse5: 8.0.3
parse5: 7.3.0
@@ -22163,7 +22163,7 @@ snapshots:
hast-util-from-parse5@8.0.3:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/unist': 3.0.3
devlop: 1.1.0
hastscript: 9.0.1
@@ -22174,23 +22174,23 @@ snapshots:
hast-util-has-property@3.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-heading-rank@3.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-is-element@3.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-parse-selector@4.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-raw@9.1.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/unist': 3.0.3
'@ungap/structured-clone': 1.3.0
hast-util-from-parse5: 8.0.3
@@ -22206,7 +22206,7 @@ snapshots:
hast-util-select@6.0.4:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/unist': 3.0.3
bcp-47-match: 2.0.3
comma-separated-tokens: 2.0.3
@@ -22226,7 +22226,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.8
'@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
comma-separated-tokens: 2.0.3
devlop: 1.1.0
estree-util-attach-comments: 3.0.0
@@ -22245,7 +22245,7 @@ snapshots:
hast-util-to-html@9.0.5:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/unist': 3.0.3
ccount: 2.0.1
comma-separated-tokens: 2.0.3
@@ -22260,7 +22260,7 @@ snapshots:
hast-util-to-jsx-runtime@2.3.6:
dependencies:
'@types/estree': 1.0.8
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/unist': 3.0.3
comma-separated-tokens: 2.0.3
devlop: 1.1.0
@@ -22279,7 +22279,7 @@ snapshots:
hast-util-to-parse5@8.0.1:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
comma-separated-tokens: 2.0.3
devlop: 1.1.0
property-information: 7.1.0
@@ -22289,22 +22289,22 @@ snapshots:
hast-util-to-string@3.0.1:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-to-text@4.0.2:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/unist': 3.0.3
hast-util-is-element: 3.0.0
unist-util-find-after: 5.0.0
hast-util-whitespace@3.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hastscript@9.0.1:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 4.0.0
property-information: 7.1.0
@@ -22735,7 +22735,7 @@ snapshots:
kleur@4.1.5: {}
- knip@5.82.1(@types/node@22.19.19)(typescript@6.0.3):
+ knip@5.82.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@22.19.19)(typescript@6.0.3):
dependencies:
'@nodelib/fs.walk': 1.2.8
'@types/node': 22.19.19
@@ -22744,7 +22744,7 @@ snapshots:
jiti: 2.6.1
js-yaml: 4.3.0
minimist: 1.2.8
- oxc-resolver: 11.17.1
+ oxc-resolver: 11.17.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
picocolors: 1.1.1
picomatch: 4.0.4
smol-toml: 1.6.1
@@ -23121,7 +23121,7 @@ snapshots:
mdast-util-math@3.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdast': 4.0.4
devlop: 1.1.0
longest-streak: 3.1.0
@@ -23134,7 +23134,7 @@ snapshots:
mdast-util-mdx-expression@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.2
@@ -23145,7 +23145,7 @@ snapshots:
mdast-util-mdx-jsx@3.2.0:
dependencies:
'@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
ccount: 2.0.1
@@ -23172,7 +23172,7 @@ snapshots:
mdast-util-mdxjs-esm@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdast': 4.0.4
devlop: 1.1.0
mdast-util-from-markdown: 2.0.2
@@ -23187,7 +23187,7 @@ snapshots:
mdast-util-to-hast@13.2.1:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdast': 4.0.4
'@ungap/structured-clone': 1.3.0
devlop: 1.1.0
@@ -23862,7 +23862,7 @@ snapshots:
- debug
- supports-color
- oxc-resolver@11.17.1:
+ oxc-resolver@11.17.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1):
optionalDependencies:
'@oxc-resolver/binding-android-arm-eabi': 11.17.1
'@oxc-resolver/binding-android-arm64': 11.17.1
@@ -23880,7 +23880,7 @@ snapshots:
'@oxc-resolver/binding-linux-x64-gnu': 11.17.1
'@oxc-resolver/binding-linux-x64-musl': 11.17.1
'@oxc-resolver/binding-openharmony-arm64': 11.17.1
- '@oxc-resolver/binding-wasm32-wasi': 11.17.1
+ '@oxc-resolver/binding-wasm32-wasi': 11.17.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
'@oxc-resolver/binding-win32-arm64-msvc': 11.17.1
'@oxc-resolver/binding-win32-ia32-msvc': 11.17.1
'@oxc-resolver/binding-win32-x64-msvc': 11.17.1
@@ -24667,7 +24667,7 @@ snapshots:
rehype-autolink-headings@7.1.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@ungap/structured-clone': 1.3.0
hast-util-heading-rank: 3.0.0
hast-util-is-element: 3.0.0
@@ -24676,7 +24676,7 @@ snapshots:
rehype-mathjax@7.1.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mathjax': 0.0.40
hast-util-to-text: 4.0.2
hastscript: 9.0.1
@@ -24687,13 +24687,13 @@ snapshots:
rehype-parse@9.0.1:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-from-html: 2.0.3
unified: 11.0.5
rehype-pretty-code@0.14.3(shiki@4.0.2):
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-to-string: 3.0.1
parse-numeric-range: 1.3.0
rehype-parse: 9.0.1
@@ -24703,21 +24703,21 @@ snapshots:
rehype-raw@7.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-raw: 9.1.0
vfile: 6.0.3
rehype-recma@1.0.0:
dependencies:
'@types/estree': 1.0.8
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-to-estree: 3.1.3
transitivePeerDependencies:
- supports-color
rehype-slug@6.0.0:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
github-slugger: 2.0.0
hast-util-heading-rank: 3.0.0
hast-util-to-string: 3.0.1
@@ -24725,7 +24725,7 @@ snapshots:
rehype-stringify@10.0.1:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
hast-util-to-html: 9.0.5
unified: 11.0.5
@@ -24777,7 +24777,7 @@ snapshots:
remark-rehype@11.1.2:
dependencies:
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdast': 4.0.4
mdast-util-to-hast: 13.2.1
unified: 11.0.5
@@ -24959,18 +24959,22 @@ snapshots:
optionalDependencies:
'@parcel/watcher': 2.5.6
- satteri@0.9.1:
+ satteri@0.10.3:
dependencies:
'@types/estree-jsx': 1.0.5
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
optionalDependencies:
- '@bruits/satteri-darwin-arm64': 0.9.1
- '@bruits/satteri-darwin-x64': 0.9.1
- '@bruits/satteri-linux-x64-gnu': 0.9.1
- '@bruits/satteri-wasm32-wasi': 0.9.1
- '@bruits/satteri-win32-x64-msvc': 0.9.1
+ '@bruits/satteri-darwin-arm64': 0.10.3
+ '@bruits/satteri-darwin-x64': 0.10.3
+ '@bruits/satteri-linux-arm64-gnu': 0.10.3
+ '@bruits/satteri-linux-arm64-musl': 0.10.3
+ '@bruits/satteri-linux-x64-gnu': 0.10.3
+ '@bruits/satteri-linux-x64-musl': 0.10.3
+ '@bruits/satteri-wasm32-wasi': 0.10.3
+ '@bruits/satteri-win32-arm64-msvc': 0.10.3
+ '@bruits/satteri-win32-x64-msvc': 0.10.3
sax@1.6.0: {}
@@ -25131,7 +25135,7 @@ snapshots:
'@shikijs/themes': 4.0.2
'@shikijs/types': 4.0.2
'@shikijs/vscode-textmate': 10.0.2
- '@types/hast': 3.0.4
+ '@types/hast': 3.0.5
side-channel-list@1.0.0:
dependencies:
From 0a22ff5b7e4600356ccabfe571b7ffdad76064d7 Mon Sep 17 00:00:00 2001
From: Aleksandr Smyshliaev
Date: Wed, 19 Aug 2026 14:45:17 +0400
Subject: [PATCH 3/8] fix(vercel): run edge middleware when isr is enabled
(#17687)
---
.changeset/isr-edge-middleware.md | 7 +
packages/integrations/vercel/src/index.ts | 46 ++--
.../vercel/src/serverless/entrypoint.ts | 7 +-
.../vercel/src/serverless/middleware.ts | 38 ++-
.../isr-edge-no-middleware/astro.config.mjs | 13 +
.../isr-edge-no-middleware/package.json | 9 +
.../src/pages/index.astro | 8 +
.../isr-with-edge-middleware/astro.config.mjs | 16 ++
.../isr-with-edge-middleware/package.json | 9 +
.../src/middleware.js | 10 +
.../src/pages/[slug].astro | 13 +
.../src/pages/api/data.ts | 3 +
.../src/pages/cached/[id].astro | 12 +
.../src/pages/index.astro | 8 +
.../src/pages/live.astro | 12 +
.../src/pages/static.astro | 12 +
.../vercel/test/isr-edge-middleware.test.ts | 250 ++++++++++++++++++
pnpm-lock.yaml | 18 ++
18 files changed, 473 insertions(+), 18 deletions(-)
create mode 100644 .changeset/isr-edge-middleware.md
create mode 100644 packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/astro.config.mjs
create mode 100644 packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/package.json
create mode 100644 packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/src/pages/index.astro
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/astro.config.mjs
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/package.json
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/middleware.js
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/[slug].astro
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/api/data.ts
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/cached/[id].astro
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/index.astro
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/live.astro
create mode 100644 packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/static.astro
create mode 100644 packages/integrations/vercel/test/isr-edge-middleware.test.ts
diff --git a/.changeset/isr-edge-middleware.md b/.changeset/isr-edge-middleware.md
new file mode 100644
index 000000000000..308518ac356c
--- /dev/null
+++ b/.changeset/isr-edge-middleware.md
@@ -0,0 +1,7 @@
+---
+'@astrojs/vercel': patch
+---
+
+Fixes `middlewareMode: 'edge'` not running your middleware when `isr` is also enabled
+
+Previously, enabling both options deployed the edge middleware but never reached it: requests went straight to the ISR function, which skips rendering entirely on a cache hit. Middleware now runs at the edge for ISR-backed routes before the cached response is served, and query strings are preserved when it forwards the request.
diff --git a/packages/integrations/vercel/src/index.ts b/packages/integrations/vercel/src/index.ts
index 93705420d940..24b5a4741018 100644
--- a/packages/integrations/vercel/src/index.ts
+++ b/packages/integrations/vercel/src/index.ts
@@ -32,7 +32,7 @@ import {
getInjectableWebAnalyticsContent,
type VercelWebAnalyticsConfig,
} from './lib/web-analytics.js';
-import { generateEdgeMiddleware } from './serverless/middleware.js';
+import { generateEdgeMiddleware, type IsrForwarding } from './serverless/middleware.js';
import { createConfigPlugin } from './vite-plugin-config.js';
const PACKAGE_NAME = '@astrojs/vercel';
@@ -468,6 +468,10 @@ export default function vercelAdapter({
);
const entryFile = new URL(_serverEntry, _buildTempFolder);
+ // Routes the edge middleware forwards to `_isr` instead of `_render`.
+ const isrRoutes: string[] = [];
+ // Checked first: a dynamic ISR route can also match an excluded path.
+ const isrExcludedRoutes: string[] = [];
if (_buildOutput === 'server' && isr) {
const isrConfig = typeof isr === 'object' ? isr : {};
await builder.buildServerlessFolder(entryFile, NODE_PATH, _config.root);
@@ -485,10 +489,9 @@ export default function vercelAdapter({
const dest = _middlewareEntryPoint ? MIDDLEWARE_PATH : NODE_PATH;
for (const route of expandedExclusions) {
// vercel interprets src as a regex pattern, so we need to escape it
- routeDefinitions.push({
- src: escapeRegex(route),
- dest,
- });
+ const src = escapeRegex(route);
+ routeDefinitions.push({ src, dest });
+ isrExcludedRoutes.push(src);
}
}
await builder.buildISRFolder(entryFile, '_isr', isrConfig, _config.root);
@@ -504,15 +507,21 @@ export default function vercelAdapter({
if (!excludeRouteFromIsr) {
const src = route.patternRegex.source;
- const dest =
- src.startsWith('^\\/_image') || src.startsWith('^\\/_server-islands')
- ? NODE_PATH
- : getIsrPath(middlewareSecret);
- if (!route.isPrerendered)
- routeDefinitions.push({
- src,
- dest,
- });
+ const isInternal =
+ src.startsWith('^\\/_image') || src.startsWith('^\\/_server-islands');
+
+ let dest = getIsrPath(middlewareSecret);
+ if (isInternal) {
+ dest = NODE_PATH;
+ } else if (_middlewareEntryPoint) {
+ // The middleware has to run before the cache is consulted.
+ dest = MIDDLEWARE_PATH;
+ }
+
+ if (!route.isPrerendered) {
+ routeDefinitions.push({ src, dest });
+ if (!isInternal) isrRoutes.push(src);
+ }
}
}
} else {
@@ -532,6 +541,7 @@ export default function vercelAdapter({
_middlewareEntryPoint,
MIDDLEWARE_PATH,
middlewareSecret,
+ { isrRoutes, isrExcludedRoutes },
);
}
}
@@ -749,7 +759,12 @@ class VercelBuilder {
});
}
- async buildMiddlewareFolder(entry: URL, functionName: string, middlewareSecret: string) {
+ async buildMiddlewareFolder(
+ entry: URL,
+ functionName: string,
+ middlewareSecret: string,
+ isrForwarding?: IsrForwarding,
+ ) {
const functionFolder = new URL(`./functions/${functionName}.func/`, this.outDir);
await generateEdgeMiddleware(
@@ -759,6 +774,7 @@ class VercelBuilder {
new URL('./middleware.mjs', functionFolder),
middlewareSecret,
this.logger,
+ isrForwarding,
);
await writeJson(new URL(`./.vc-config.json`, functionFolder), {
diff --git a/packages/integrations/vercel/src/serverless/entrypoint.ts b/packages/integrations/vercel/src/serverless/entrypoint.ts
index 93bf48ff0333..29272688ff78 100644
--- a/packages/integrations/vercel/src/serverless/entrypoint.ts
+++ b/packages/integrations/vercel/src/serverless/entrypoint.ts
@@ -30,7 +30,12 @@ export default {
realPath = url.searchParams.get(ASTRO_PATH_PARAM);
}
if (typeof realPath === 'string') {
- url.pathname = realPath;
+ // The header carries the client's whole path, query included; the route
+ // rewrite carries only the pathname and leaves the query on this request.
+ const target = new URL(realPath, url);
+ const search = target.search || url.search;
+ url.pathname = target.pathname;
+ url.search = search;
// Remove the internal routing params so they never reach user code.
url.searchParams.delete(ASTRO_PATH_PARAM);
url.searchParams.delete(ASTRO_PATH_TOKEN_PARAM);
diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts
index 058d78296ba3..cef2856070ed 100644
--- a/packages/integrations/vercel/src/serverless/middleware.ts
+++ b/packages/integrations/vercel/src/serverless/middleware.ts
@@ -6,9 +6,18 @@ import {
ASTRO_LOCALS_HEADER,
ASTRO_MIDDLEWARE_SECRET_HEADER,
ASTRO_PATH_HEADER,
+ ASTRO_PATH_PARAM,
+ ASTRO_PATH_TOKEN_PARAM,
NODE_PATH,
} from '../index.js';
+export interface IsrForwarding {
+ /** Route patterns backed by the ISR function. */
+ isrRoutes: string[];
+ /** Route patterns `isr.exclude` keeps out of the cache; checked first. */
+ isrExcludedRoutes: string[];
+}
+
/**
* It generates the Vercel Edge Middleware file.
*
@@ -22,6 +31,7 @@ import {
* @param outPath
* @param middlewareSecret
* @param logger
+ * @param isrForwarding Route patterns the generated `next()` forwards to `_isr`, if any
* @returns {Promise} The path to the bundled file
*/
export async function generateEdgeMiddleware(
@@ -31,12 +41,14 @@ export async function generateEdgeMiddleware(
outPath: URL,
middlewareSecret: string,
logger: AstroIntegrationLogger,
+ isrForwarding?: IsrForwarding,
): Promise {
const code = edgeMiddlewareTemplate(
astroMiddlewareEntryPointPath,
vercelEdgeMiddlewareHandlerPath,
middlewareSecret,
logger,
+ isrForwarding,
);
// https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware
const bundledFilePath = fileURLToPath(outPath);
@@ -96,6 +108,7 @@ function edgeMiddlewareTemplate(
vercelEdgeMiddlewareHandlerPath: URL,
middlewareSecret: string,
logger: AstroIntegrationLogger,
+ isrForwarding?: IsrForwarding,
) {
const middlewarePath = JSON.stringify(
fileURLToPath(astroMiddlewareEntryPointPath).replace(/\\/g, '/'),
@@ -116,6 +129,27 @@ function edgeMiddlewareTemplate(
${handlerTemplateImport}
import { onRequest } from ${middlewarePath};
import { createContext, trySerializeLocals } from 'astro/middleware';
+
+const isrRoutes = ${JSON.stringify(isrForwarding?.isrRoutes ?? [])}.map((source) => new RegExp(source));
+const isrExcludedRoutes = ${JSON.stringify(isrForwarding?.isrExcludedRoutes ?? [])}.map(
+ (source) => new RegExp(source),
+);
+
+const isCached = (pathname) =>
+ !isrExcludedRoutes.some((route) => route.test(pathname)) &&
+ isrRoutes.some((route) => route.test(pathname));
+
+// Cached routes go back through \`_isr\`, or every request would re-render.
+// Keep in step with \`getIsrPath\`.
+function forwardPath(pathname) {
+ if (!isCached(pathname)) return '/${NODE_PATH}';
+ const params = new URLSearchParams({
+ '${ASTRO_PATH_PARAM}': pathname,
+ '${ASTRO_PATH_TOKEN_PARAM}': '${middlewareSecret}',
+ });
+ return '/_isr?' + params.toString();
+}
+
export default async function middleware(request, context) {
const ctx = createContext({
request,
@@ -123,10 +157,10 @@ export default async function middleware(request, context) {
clientAddress: request.headers.get('x-real-ip') || undefined,
});
Object.assign(ctx.locals, { vercel: { edge: context }, ...${handlerTemplateCall} });
- const { origin } = new URL(request.url);
+ const { origin, pathname } = new URL(request.url);
const next = async () => {
const { vercel, ...locals } = ctx.locals;
- const response = await fetch(new URL('/${NODE_PATH}', request.url), {
+ const response = await fetch(new URL(forwardPath(pathname), request.url), {
method: request.method,
headers: {
...Object.fromEntries(request.headers.entries()),
diff --git a/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/astro.config.mjs b/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/astro.config.mjs
new file mode 100644
index 000000000000..0d5bf7fe6c2e
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/astro.config.mjs
@@ -0,0 +1,13 @@
+import vercel from '@astrojs/vercel';
+import { defineConfig } from 'astro/config';
+
+// `middlewareMode: 'edge'` with no middleware file in the project.
+export default defineConfig({
+ output: 'server',
+ adapter: vercel({
+ middlewareMode: 'edge',
+ isr: {
+ expiration: 120,
+ },
+ }),
+});
diff --git a/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/package.json b/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/package.json
new file mode 100644
index 000000000000..135b27154a2b
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/vercel-isr-edge-no-middleware",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/vercel": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/src/pages/index.astro
new file mode 100644
index 000000000000..72ef33dae11d
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-edge-no-middleware/src/pages/index.astro
@@ -0,0 +1,8 @@
+
+
+ No middleware
+
+
+ Testing
+
+
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/astro.config.mjs b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/astro.config.mjs
new file mode 100644
index 000000000000..0e637d6f45f0
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/astro.config.mjs
@@ -0,0 +1,16 @@
+import vercel from '@astrojs/vercel';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ output: 'server',
+ redirects: {
+ '/old': '/',
+ },
+ adapter: vercel({
+ middlewareMode: 'edge',
+ isr: {
+ expiration: 120,
+ exclude: ['/live'],
+ },
+ }),
+});
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/package.json b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/package.json
new file mode 100644
index 000000000000..7fc8a90fda5b
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/vercel-isr-with-edge-middleware",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/vercel": "workspace:*",
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/middleware.js b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/middleware.js
new file mode 100644
index 000000000000..98edbe59e577
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/middleware.js
@@ -0,0 +1,10 @@
+/**
+ * Reads the response before returning it: `next()` has to hand one back.
+ *
+ * @type {import("astro").MiddlewareResponseHandler}
+ */
+export const onRequest = async (_context, next) => {
+ const response = await next();
+ response.headers.set('x-astro-middleware', 'ran');
+ return response;
+};
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/[slug].astro b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/[slug].astro
new file mode 100644
index 000000000000..f137f8e453b9
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/[slug].astro
@@ -0,0 +1,13 @@
+---
+// Its pattern also matches `/live`, which `isr.exclude` keeps out of the cache.
+export const prerender = false;
+---
+
+
+
+ {Astro.params.slug}
+
+
+ {Astro.params.slug}
+
+
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/api/data.ts b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/api/data.ts
new file mode 100644
index 000000000000..39aa7c5111df
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/api/data.ts
@@ -0,0 +1,3 @@
+import type { APIRoute } from 'astro';
+
+export const GET: APIRoute = ({ url }) => Response.json({ ok: true, query: url.search });
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/cached/[id].astro b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/cached/[id].astro
new file mode 100644
index 000000000000..aa3226189a5c
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/cached/[id].astro
@@ -0,0 +1,12 @@
+---
+const { id } = Astro.params;
+---
+
+
+
+ Cached {id}
+
+
+ {id}
+
+
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/index.astro b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/index.astro
new file mode 100644
index 000000000000..3c0039ba0b2f
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/index.astro
@@ -0,0 +1,8 @@
+
+
+ ISR with edge middleware
+
+
+ Testing
+
+
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/live.astro b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/live.astro
new file mode 100644
index 000000000000..6147ae65e7a5
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/live.astro
@@ -0,0 +1,12 @@
+---
+// Listed in `isr.exclude`, so it must reach `_render` rather than the cache.
+---
+
+
+
+ Live
+
+
+ Live
+
+
diff --git a/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/static.astro b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/static.astro
new file mode 100644
index 000000000000..c6aefcb94ef9
--- /dev/null
+++ b/packages/integrations/vercel/test/fixtures/isr-with-edge-middleware/src/pages/static.astro
@@ -0,0 +1,12 @@
+---
+export const prerender = true;
+---
+
+
+
+ Static
+
+
+ Static
+
+
diff --git a/packages/integrations/vercel/test/isr-edge-middleware.test.ts b/packages/integrations/vercel/test/isr-edge-middleware.test.ts
new file mode 100644
index 000000000000..6b4da7960f04
--- /dev/null
+++ b/packages/integrations/vercel/test/isr-edge-middleware.test.ts
@@ -0,0 +1,250 @@
+import assert from 'node:assert/strict';
+import { before, describe, it } from 'node:test';
+import { type Fixture, getVercelConfig, loadFixture } from './test-utils.ts';
+
+const destOf = (routes: { src?: string; dest?: string }[], src: string) =>
+ routes.find((route) => route.src === src)?.dest;
+
+/**
+ * With `isr`, every route used to be pointed straight at the ISR function, so
+ * the middleware edge function was built and deployed but never invoked — and
+ * ISR skips the function entirely on a cache hit, so middleware stopped running
+ * once an entry was warm.
+ */
+describe('ISR with edge middleware', () => {
+ let fixture: Fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/isr-with-edge-middleware/',
+ });
+ await fixture.build({});
+ });
+
+ /** The URL `next()` forwards to, with `fetch` stubbed out. */
+ async function forwardedUrl(path: string): Promise {
+ const entry = new URL(
+ '../.vercel/output/functions/_middleware.func/middleware.mjs',
+ fixture.config.outDir,
+ );
+ const module = await import(entry.href);
+
+ const originalFetch = globalThis.fetch;
+ let captured: string | undefined;
+ globalThis.fetch = async (url) => {
+ captured = String(url);
+ return new Response('ok');
+ };
+ try {
+ await module.default(new Request(`http://example.com${path}`), {});
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+
+ assert.ok(captured, 'next() did not forward the request');
+ return new URL(captured);
+ }
+
+ it('builds the middleware edge function', { timeout: 30000 }, async () => {
+ const config = JSON.parse(
+ await fixture.readFile('../.vercel/output/functions/_middleware.func/.vc-config.json'),
+ );
+ assert.equal(config.runtime, 'edge');
+ assert.equal(config.entrypoint, 'middleware.mjs');
+ });
+
+ it('points page routes at the middleware', { timeout: 30000 }, async () => {
+ const { routes } = await getVercelConfig(fixture);
+
+ // Not `routes.some((r) => r.dest === '_middleware')`: the 404 catch-all
+ // always points there, so that passes even when no page route does.
+ assert.equal(destOf(routes, '^/$'), '_middleware');
+ assert.equal(destOf(routes, '^/cached/([^/]+?)/?$'), '_middleware');
+ });
+
+ it('points routes excluded from ISR at the middleware too', { timeout: 30000 }, async () => {
+ const { routes } = await getVercelConfig(fixture);
+ assert.equal(destOf(routes, '^/live$'), '_middleware');
+ });
+
+ it('leaves internal endpoints on the serverless function', { timeout: 30000 }, async () => {
+ const { routes } = await getVercelConfig(fixture);
+ assert.equal(destOf(routes, '^/_image/?$'), '_render');
+ assert.equal(destOf(routes, '^/_server-islands/([^/]+?)/?$'), '_render');
+ });
+
+ it('routes every on-demand route kind through the middleware', {
+ timeout: 30000,
+ }, async () => {
+ const { routes } = await getVercelConfig(fixture);
+ assert.equal(destOf(routes, '^/api/data/?$'), '_middleware', 'endpoint');
+ assert.equal(destOf(routes, '^/404/?$'), '_middleware', '404 page');
+ });
+
+ it('leaves prerendered pages as static files', { timeout: 30000 }, async () => {
+ const { routes } = await getVercelConfig(fixture);
+ assert.equal(destOf(routes, '^/static/?$'), undefined, 'no route: the file is served directly');
+
+ const html = await fixture.readFile('../.vercel/output/static/static/index.html');
+ assert.ok(html.includes('Static
'));
+ });
+
+ it('keeps redirects ahead of the middleware', { timeout: 30000 }, async () => {
+ const { routes } = await getVercelConfig(fixture);
+ const redirect = routes.find((route) => route.src === '^/old$');
+ const filesystem = routes.findIndex((route) => route.handle === 'filesystem');
+
+ assert.equal(redirect?.status, 301);
+ assert.equal(redirect?.headers.Location, '/');
+ assert.ok(routes.indexOf(redirect!) < filesystem, 'redirects resolve before anything else');
+ });
+
+ it('keeps the ISR config', { timeout: 30000 }, async () => {
+ const config = JSON.parse(
+ await fixture.readFile('../.vercel/output/functions/_isr.prerender-config.json'),
+ );
+ assert.equal(config.expiration, 120);
+ });
+
+ it('forwards cached routes to the ISR function', { timeout: 30000 }, async () => {
+ const url = await forwardedUrl('/');
+ assert.equal(url.pathname, '/_isr');
+ assert.equal(url.searchParams.get('x_astro_path'), '/');
+ assert.ok(
+ url.searchParams.get('x_astro_path_token'),
+ 'the entrypoint ignores an untokened path',
+ );
+ });
+
+ it('forwards endpoints to the ISR function too', { timeout: 30000 }, async () => {
+ const url = await forwardedUrl('/api/data');
+ assert.equal(url.pathname, '/_isr');
+ assert.equal(url.searchParams.get('x_astro_path'), '/api/data');
+ });
+
+ it('forwards dynamic routes with their real path', { timeout: 30000 }, async () => {
+ const url = await forwardedUrl('/cached/42');
+ assert.equal(url.pathname, '/_isr');
+ // The path is the ISR cache key. Getting it wrong gives every page one entry.
+ assert.equal(url.searchParams.get('x_astro_path'), '/cached/42');
+ });
+
+ it('forwards routes excluded from ISR to the serverless function', {
+ timeout: 30000,
+ }, async () => {
+ // `/live` also matches the `[slug]` route, which is cached: an exclusion
+ // only holds if it is checked before the ISR patterns.
+ const url = await forwardedUrl('/live');
+ assert.equal(url.pathname, '/_render');
+ });
+
+ it('still caches the dynamic route the exclusion overlaps', {
+ timeout: 30000,
+ }, async () => {
+ const url = await forwardedUrl('/anything-else');
+ assert.equal(url.pathname, '/_isr');
+ assert.equal(url.searchParams.get('x_astro_path'), '/anything-else');
+ });
+
+ it('renders paths no route matches instead of caching them', {
+ timeout: 30000,
+ }, async () => {
+ // The 404 catch-all reaches the middleware too. Forwarding those to `_isr`
+ // would give every unmatched URL a cache entry.
+ const url = await forwardedUrl('/no/such/page');
+ assert.equal(url.pathname, '/_render');
+ });
+
+ it('keeps the query string out of the cache key', { timeout: 30000 }, async () => {
+ const url = await forwardedUrl('/?foo=1');
+ assert.equal(url.searchParams.get('x_astro_path'), '/');
+ });
+
+ /** The build's middleware secret, read back out of the generated middleware. */
+ async function middlewareSecret(): Promise {
+ const bundle = await fixture.readFile(
+ '../.vercel/output/functions/_middleware.func/middleware.mjs',
+ );
+ const secret = (/"x-astro-middleware-secret":\s*"([^"]+)"/.exec(bundle))?.[1];
+ assert.ok(secret, 'the generated middleware carries no secret');
+ return secret;
+ }
+
+ async function isrFetch(path: string, headers: Record = {}) {
+ const functionConfig = JSON.parse(
+ await fixture.readFile('../.vercel/output/functions/_isr.func/.vc-config.json'),
+ );
+ const entry = new URL(
+ `../.vercel/output/functions/_isr.func/${functionConfig.handler}`,
+ fixture.config.outDir,
+ );
+ const module = await import(entry.href);
+
+ return module.default.fetch(new Request(`https://example.com${path}`, { headers }));
+ }
+
+ it('keeps the query string the middleware forwarded', { timeout: 30000 }, async () => {
+ const secret = await middlewareSecret();
+ // The query rides the header, not the forward URL: it must not be swallowed
+ // by the path override.
+ const response = await isrFetch(
+ `/_isr?x_astro_path=%2Fapi%2Fdata&x_astro_path_token=${secret}`,
+ {
+ 'x-astro-middleware-secret': secret,
+ 'x-astro-path': '/api/data?foo=bar',
+ },
+ );
+
+ assert.deepEqual(await response.json(), { ok: true, query: '?foo=bar' });
+ });
+
+ it('keeps the query string the route rewrite passed through', { timeout: 30000 }, async () => {
+ const secret = await middlewareSecret();
+ // No middleware header: `passQuery` leaves the client's query on the request
+ // while the path arrives as a param.
+ const response = await isrFetch(
+ `/_isr?x_astro_path=%2Fapi%2Fdata&x_astro_path_token=${secret}&foo=bar`,
+ );
+
+ assert.deepEqual(await response.json(), { ok: true, query: '?foo=bar' });
+ });
+
+ it('still returns the response to the middleware', { timeout: 30000 }, async () => {
+ const entry = new URL(
+ '../.vercel/output/functions/_middleware.func/middleware.mjs',
+ fixture.config.outDir,
+ );
+ const module = await import(entry.href);
+
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = async () => new Response('ok');
+ try {
+ const response = await module.default(new Request('http://example.com/'), {});
+ assert.equal(response.headers.get('x-astro-middleware'), 'ran');
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+});
+
+describe('ISR with edge middleware requested but no middleware file', () => {
+ let fixture: Fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/isr-edge-no-middleware/',
+ });
+ await fixture.build({});
+ });
+
+ it('routes straight to the ISR function', { timeout: 30000 }, async () => {
+ const { routes } = await getVercelConfig(fixture);
+ assert.match(destOf(routes, '^/$') ?? '', /^\/_isr\?/);
+ });
+
+ it('builds no middleware function', { timeout: 30000 }, async () => {
+ await assert.rejects(() =>
+ fixture.readFile('../.vercel/output/functions/_middleware.func/.vc-config.json'),
+ );
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1756ab5bfee6..90314c03aa58 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6381,6 +6381,24 @@ importers:
specifier: workspace:*
version: link:../../../../../astro
+ packages/integrations/vercel/test/fixtures/isr-edge-no-middleware:
+ dependencies:
+ '@astrojs/vercel':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: workspace:*
+ version: link:../../../../../astro
+
+ packages/integrations/vercel/test/fixtures/isr-with-edge-middleware:
+ dependencies:
+ '@astrojs/vercel':
+ specifier: workspace:*
+ version: link:../../..
+ astro:
+ specifier: workspace:*
+ version: link:../../../../../astro
+
packages/integrations/vercel/test/fixtures/max-duration:
dependencies:
'@astrojs/vercel':
From cd233f80d5b67c9955c68711ffce6723984fdc85 Mon Sep 17 00:00:00 2001
From: Aleksandr Smyshliaev
Date: Wed, 19 Aug 2026 10:46:32 +0000
Subject: [PATCH 4/8] [ci] format
---
packages/integrations/vercel/test/isr-edge-middleware.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/integrations/vercel/test/isr-edge-middleware.test.ts b/packages/integrations/vercel/test/isr-edge-middleware.test.ts
index 6b4da7960f04..2d838083bdad 100644
--- a/packages/integrations/vercel/test/isr-edge-middleware.test.ts
+++ b/packages/integrations/vercel/test/isr-edge-middleware.test.ts
@@ -165,7 +165,7 @@ describe('ISR with edge middleware', () => {
const bundle = await fixture.readFile(
'../.vercel/output/functions/_middleware.func/middleware.mjs',
);
- const secret = (/"x-astro-middleware-secret":\s*"([^"]+)"/.exec(bundle))?.[1];
+ const secret = /"x-astro-middleware-secret":\s*"([^"]+)"/.exec(bundle)?.[1];
assert.ok(secret, 'the generated middleware carries no secret');
return secret;
}
From 05763a0884aabb1da78a2749d5bb9d41ae620527 Mon Sep 17 00:00:00 2001
From: Matthew Phillips
Date: Wed, 19 Aug 2026 09:31:57 -0400
Subject: [PATCH 5/8] Respect path-segment boundaries when stripping the
configured base (#17701)
* Respect path-segment boundaries when stripping the configured base
* Use stripRequestBase in matchRequest and cover the app.match path
---
.changeset/nine-colts-add.md | 6 +
packages/astro/src/core/app/base.ts | 11 +-
packages/astro/src/core/fetch/fetch-state.ts | 17 +-
packages/astro/src/core/i18n/domain.ts | 19 +--
.../astro/src/core/routing/match-request.ts | 23 +--
.../units/app/base-prefix-boundary.test.ts | 146 ++++++++++++++++++
packages/internal-helpers/src/path.ts | 22 +++
7 files changed, 186 insertions(+), 58 deletions(-)
create mode 100644 .changeset/nine-colts-add.md
create mode 100644 packages/astro/test/units/app/base-prefix-boundary.test.ts
diff --git a/.changeset/nine-colts-add.md b/.changeset/nine-colts-add.md
new file mode 100644
index 000000000000..c93fb193a4c2
--- /dev/null
+++ b/.changeset/nine-colts-add.md
@@ -0,0 +1,6 @@
+---
+'astro': patch
+'@astrojs/internal-helpers': patch
+---
+
+Fixes base path stripping to respect path-segment boundaries. With a configured `base` such as `/docs`, a request like `/docs-archive/page` is no longer treated as being under the base, so routing and `context.url.pathname` now agree on the same pathname.
diff --git a/packages/astro/src/core/app/base.ts b/packages/astro/src/core/app/base.ts
index 0f810ea76022..e7daceb8bf08 100644
--- a/packages/astro/src/core/app/base.ts
+++ b/packages/astro/src/core/app/base.ts
@@ -1,7 +1,7 @@
import {
- collapseDuplicateLeadingSlashes,
prependForwardSlash,
removeTrailingForwardSlash,
+ stripRequestBase,
} from '@astrojs/internal-helpers/path';
import { matchPattern } from '@astrojs/internal-helpers/remote';
import { computePathnameFromDomain } from '../i18n/domain.js';
@@ -281,14 +281,7 @@ export abstract class BaseApp {
}
public removeBase(pathname: string) {
- // Collapse multiple leading slashes to prevent middleware authorization bypass.
- // Without this, `//admin` would be treated as starting with base `/` and sliced
- // to `/admin` for routing, while middleware still sees `//admin` in the URL.
- pathname = collapseDuplicateLeadingSlashes(pathname);
- if (pathname.startsWith(this.manifest.base)) {
- return pathname.slice(this.baseWithoutTrailingSlash.length + 1);
- }
- return pathname;
+ return stripRequestBase(pathname, this.manifest.base);
}
/**
diff --git a/packages/astro/src/core/fetch/fetch-state.ts b/packages/astro/src/core/fetch/fetch-state.ts
index f4325c2fa20b..bd0be821aa90 100644
--- a/packages/astro/src/core/fetch/fetch-state.ts
+++ b/packages/astro/src/core/fetch/fetch-state.ts
@@ -1,9 +1,8 @@
import colors from 'piccolore';
import {
- collapseDuplicateLeadingSlashes,
collapseDuplicateSlashes,
prependForwardSlash,
- removeTrailingForwardSlash,
+ stripRequestBase,
} from '@astrojs/internal-helpers/path';
import { deserializeActionResult } from '../../actions/runtime/client.js';
import { createCallAction, createGetActionResult, hasActionPayload } from '../../actions/utils.js';
@@ -1026,18 +1025,12 @@ export class FetchState implements AstroFetchState {
* Strips the manifest's base from a normalized request pathname and prepends
* a forward slash.
*
- * Mirrors `BaseApp.removeBase`, including the
- * `collapseDuplicateLeadingSlashes` fix that prevents middleware
- * authorization bypass when the URL starts with `//`.
+ * Mirrors `BaseApp.removeBase`: the router matches against this stripped path
+ * while middleware reads the un-stripped `context.url.pathname`, so both must
+ * strip the base identically.
*/
#computePathname(normalizedPathname: string): string {
- let pathname = collapseDuplicateLeadingSlashes(normalizedPathname);
- const base = this.manifest.base;
- if (pathname.startsWith(base)) {
- const baseWithoutTrailingSlash = removeTrailingForwardSlash(base);
- pathname = pathname.slice(baseWithoutTrailingSlash.length + 1);
- }
- return prependForwardSlash(pathname);
+ return prependForwardSlash(stripRequestBase(normalizedPathname, this.manifest.base));
}
/**
diff --git a/packages/astro/src/core/i18n/domain.ts b/packages/astro/src/core/i18n/domain.ts
index b04a27d1e8c1..110d28012e7e 100644
--- a/packages/astro/src/core/i18n/domain.ts
+++ b/packages/astro/src/core/i18n/domain.ts
@@ -1,9 +1,9 @@
import {
appendForwardSlash,
- collapseDuplicateLeadingSlashes,
joinPaths,
prependForwardSlash,
removeTrailingForwardSlash,
+ stripRequestBase,
} from '@astrojs/internal-helpers/path';
import { normalizeTheLocale } from '../../i18n/path.js';
import type { SSRManifest } from '../app/types.js';
@@ -72,13 +72,13 @@ export function computePathnameFromDomain(
}
if (locale) {
- const requestPathname = pathnameFromRequest ?? removeBase(url.pathname, base);
+ const requestPathname = pathnameFromRequest ?? stripRequestBase(url.pathname, base);
pathname = prependForwardSlash(joinPaths(normalizeTheLocale(locale), requestPathname));
if (trailingSlash === 'always') {
pathname = appendForwardSlash(pathname);
} else if (trailingSlash === 'never') {
pathname = removeTrailingForwardSlash(pathname);
- } else if (requestPathname.endsWith('/')) {
+ } else if (url.pathname.endsWith('/')) {
// trailingSlash === 'ignore': preserve the original trailing slash
pathname = appendForwardSlash(pathname);
}
@@ -94,16 +94,3 @@ export function computePathnameFromDomain(
}
return pathname;
}
-
-/**
- * Mirror of `BaseApp.removeBase`, including the
- * `collapseDuplicateLeadingSlashes` fix that prevents middleware
- * authorization bypass when the URL starts with `//`.
- */
-function removeBase(pathname: string, base: string): string {
- pathname = collapseDuplicateLeadingSlashes(pathname);
- if (pathname.startsWith(base)) {
- return pathname.slice(removeTrailingForwardSlash(base).length + 1);
- }
- return pathname;
-}
diff --git a/packages/astro/src/core/routing/match-request.ts b/packages/astro/src/core/routing/match-request.ts
index de63c8d5b67b..2ad59fc97bfa 100644
--- a/packages/astro/src/core/routing/match-request.ts
+++ b/packages/astro/src/core/routing/match-request.ts
@@ -1,8 +1,4 @@
-import {
- collapseDuplicateLeadingSlashes,
- prependForwardSlash,
- removeTrailingForwardSlash,
-} from '@astrojs/internal-helpers/path';
+import { prependForwardSlash, stripRequestBase } from '@astrojs/internal-helpers/path';
import type { RouteData } from '../../types/public/internal.js';
import type { SSRManifest } from '../app/types.js';
import { computePathnameFromDomain } from '../i18n/domain.js';
@@ -10,21 +6,6 @@ import { AstroIntegrationLogger } from '../logger/core.js';
import { getLogger } from '../logger/manifest-logger.js';
import { matchAllRoutes, matchRoute } from './route-table.js';
-/**
- * Strips the manifest base from a pathname (the pure counterpart of
- * `BaseApp.removeBase`). Collapses multiple leading slashes first to prevent
- * middleware authorization bypass: without this, `//admin` would be treated as
- * starting with base `/` and sliced to `/admin` for routing, while middleware
- * still sees `//admin` in the URL.
- */
-function removeBase(manifest: SSRManifest, pathname: string): string {
- pathname = collapseDuplicateLeadingSlashes(pathname);
- if (pathname.startsWith(manifest.base)) {
- return pathname.slice(removeTrailingForwardSlash(manifest.base).length + 1);
- }
- return pathname;
-}
-
/**
* Decodes a pathname with `decodeURI`, falling back to the raw pathname when it
* contains an invalid percent-sequence (e.g. `%C0%AF`, an overlong-UTF-8 encoding of
@@ -73,7 +54,7 @@ export function matchRequest(
getLogger(manifest),
);
if (!pathname) {
- pathname = prependForwardSlash(removeBase(manifest, url.pathname));
+ pathname = prependForwardSlash(stripRequestBase(url.pathname, manifest.base));
}
const routeData = matchRoute(manifest, safeDecodeURI(manifest, pathname));
if (!routeData) return undefined;
diff --git a/packages/astro/test/units/app/base-prefix-boundary.test.ts b/packages/astro/test/units/app/base-prefix-boundary.test.ts
new file mode 100644
index 000000000000..df01b9720f51
--- /dev/null
+++ b/packages/astro/test/units/app/base-prefix-boundary.test.ts
@@ -0,0 +1,146 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import type { MiddlewareHandler } from '../../../dist/types/public/common.js';
+import { App } from '../../../dist/core/app/app.js';
+import { parseRoute } from '../../../dist/core/routing/parse-route.js';
+import { createComponent, render } from '../../../dist/runtime/server/index.js';
+import { createManifest, createRouteInfo } from './test-helpers.ts';
+
+/**
+ * Tests that base stripping respects path-segment boundaries.
+ *
+ * With `base: '/app'`, only `/app` and paths under `/app/` belong to the base.
+ * Stripping the base with a plain prefix check treats `/appX/admin` as under the
+ * base and slices it to `/admin`, so the router matches `/admin` while
+ * `context.url.pathname` still reads `/appX/admin`. Middleware keyed on the
+ * base-prefixed pathname then sees a path it does not recognize.
+ */
+
+const routeOptions: Parameters[1] = {
+ config: { base: '/app', trailingSlash: 'ignore' },
+ pageExtensions: [],
+} as any;
+
+const adminRouteData = parseRoute('admin', routeOptions, {
+ component: 'src/pages/admin.astro',
+});
+
+const publicRouteData = parseRoute('index.astro', routeOptions, {
+ component: 'src/pages/index.astro',
+});
+
+const adminPage = createComponent((_result: any, _props: any, _slots: any) => {
+ return render`Admin Panel
`;
+});
+
+const publicPage = createComponent((_result: any, _props: any, _slots: any) => {
+ return render`Public
`;
+});
+
+const pageMap = new Map([
+ [
+ adminRouteData.component,
+ async () => ({
+ page: async () => ({
+ default: adminPage,
+ }),
+ }),
+ ],
+ [
+ publicRouteData.component,
+ async () => ({
+ page: async () => ({
+ default: publicPage,
+ }),
+ }),
+ ],
+]);
+
+/**
+ * Middleware that blocks access to the base-prefixed `/app/admin` route,
+ * as recommended in the official Astro authentication docs.
+ */
+function createAuthMiddleware() {
+ return (async () => ({
+ onRequest: (async (context, next) => {
+ const pathname = context.url.pathname;
+ if (pathname === '/app/admin' || pathname.startsWith('/app/admin/')) {
+ return new Response('Forbidden', { status: 403 });
+ }
+ return next();
+ }) satisfies MiddlewareHandler,
+ })) as () => Promise<{ onRequest: MiddlewareHandler }>;
+}
+
+function createApp(middleware: ReturnType) {
+ return new App(
+ createManifest({
+ base: '/app',
+ routes: [createRouteInfo(adminRouteData), createRouteInfo(publicRouteData)],
+ pageMap: pageMap as any,
+ middleware: middleware as any,
+ }) as any,
+ );
+}
+
+describe('base stripping respects path-segment boundaries', () => {
+ it('middleware blocks /app/admin with normal request', async () => {
+ const app = createApp(createAuthMiddleware());
+ const request = new Request('http://example.com/app/admin');
+ const response = await app.render(request);
+ assert.equal(response.status, 403, '/app/admin should be blocked by middleware');
+ });
+
+ it('middleware blocks /appX/admin (single-character prefix extension)', async () => {
+ const app = createApp(createAuthMiddleware());
+ const request = new Request('http://example.com/appX/admin');
+ const response = await app.render(request);
+ assert.notEqual(
+ response.status,
+ 200,
+ '/appX/admin should not resolve to the protected /admin route',
+ );
+ });
+
+ it('middleware blocks /app2/admin (digit prefix extension)', async () => {
+ const app = createApp(createAuthMiddleware());
+ const request = new Request('http://example.com/app2/admin');
+ const response = await app.render(request);
+ assert.notEqual(
+ response.status,
+ 200,
+ '/app2/admin should not resolve to the protected /admin route',
+ );
+ });
+
+ it('middleware blocks /app-/admin (punctuation prefix extension)', async () => {
+ const app = createApp(createAuthMiddleware());
+ const request = new Request('http://example.com/app-/admin');
+ const response = await app.render(request);
+ assert.notEqual(
+ response.status,
+ 200,
+ '/app-/admin should not resolve to the protected /admin route',
+ );
+ });
+
+ it('app.match does not resolve /appX/admin to the protected route', () => {
+ const app = createApp(createAuthMiddleware());
+ const request = new Request('http://example.com/appX/admin');
+ const routeData = app.match(request);
+ assert.equal(routeData, undefined, '/appX/admin should not match a route under the base');
+ });
+
+ it('middleware blocks /appX/admin when the adapter supplies routeData', async () => {
+ const app = createApp(createAuthMiddleware());
+ const request = new Request('http://example.com/appX/admin');
+ // Adapters may call app.match() and thread the result back into render().
+ const routeData = app.match(request);
+ const response = await app.render(request, { routeData });
+ assert.notEqual(
+ response.status,
+ 200,
+ '/appX/admin should not render the protected /admin route via the routeData render path',
+ );
+ });
+});
diff --git a/packages/internal-helpers/src/path.ts b/packages/internal-helpers/src/path.ts
index 9b7a82923181..368c999f470b 100644
--- a/packages/internal-helpers/src/path.ts
+++ b/packages/internal-helpers/src/path.ts
@@ -262,6 +262,28 @@ export function removeBase(path: string, base: string) {
return path;
}
+/**
+ * Strips a configured request `base` from a pathname, only when the pathname is
+ * the base itself or lies under it at a path-segment boundary. `stripRequestBase('/appX/admin', '/app')`
+ * leaves the pathname untouched, whereas `stripRequestBase('/app/admin', '/app')` returns `/admin`.
+ *
+ * Leading slashes are collapsed first so `//admin` cannot pose as being under a
+ * `/` base. Route matching and `context.url.pathname` derive from separate
+ * copies of the request path, so a mismatch here lets a request reach a route
+ * whose base-prefixed pathname middleware never observes.
+ */
+export function stripRequestBase(pathname: string, base: string): string {
+ pathname = collapseDuplicateLeadingSlashes(pathname);
+ const baseWithoutTrailingSlash = removeTrailingForwardSlash(base);
+ if (pathname === baseWithoutTrailingSlash) {
+ return '/';
+ }
+ if (pathname.startsWith(baseWithoutTrailingSlash + '/')) {
+ return pathname.slice(baseWithoutTrailingSlash.length);
+ }
+ return pathname;
+}
+
const WITH_FILE_EXT = /\/[^/]+\.\w+$/;
export function hasFileExtension(path: string) {
From a90ff6650fc244bf6ec86844a82dd13b88a4fd41 Mon Sep 17 00:00:00 2001
From: Erika <3019731+Princesseuh@users.noreply.github.com>
Date: Wed, 19 Aug 2026 15:44:27 +0200
Subject: [PATCH 6/8] fix(assets): harden vendored image-size box and entry
checks (#17747)
---
.changeset/heavy-moons-repeat.md | 5 +
.../assets/utils/vendor/image-size/LICENSE | 1 +
.../utils/vendor/image-size/types/heif.ts | 11 ++-
.../utils/vendor/image-size/types/icns.ts | 15 ++-
.../utils/vendor/image-size/types/jp2.ts | 5 +-
.../utils/vendor/image-size/types/jxl.ts | 7 +-
.../utils/vendor/image-size/types/utils.ts | 38 +++++---
.../units/assets/image-size-malformed.test.ts | 95 +++++++++++++++++++
8 files changed, 156 insertions(+), 21 deletions(-)
create mode 100644 .changeset/heavy-moons-repeat.md
create mode 100644 packages/astro/test/units/assets/image-size-malformed.test.ts
diff --git a/.changeset/heavy-moons-repeat.md b/.changeset/heavy-moons-repeat.md
new file mode 100644
index 000000000000..ea451b2261df
--- /dev/null
+++ b/.changeset/heavy-moons-repeat.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixes builds hanging when an image file is malformed
diff --git a/packages/astro/src/assets/utils/vendor/image-size/LICENSE b/packages/astro/src/assets/utils/vendor/image-size/LICENSE
index 8bdffcff7d22..991e62d6429f 100644
--- a/packages/astro/src/assets/utils/vendor/image-size/LICENSE
+++ b/packages/astro/src/assets/utils/vendor/image-size/LICENSE
@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright © 2013-Present Aditya Yadav, http://netroy.in
+Copyright © 2026 lcf2212dev
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/packages/astro/src/assets/utils/vendor/image-size/types/heif.ts b/packages/astro/src/assets/utils/vendor/image-size/types/heif.ts
index feb322f81bbe..84983b43b153 100644
--- a/packages/astro/src/assets/utils/vendor/image-size/types/heif.ts
+++ b/packages/astro/src/assets/utils/vendor/image-size/types/heif.ts
@@ -56,11 +56,14 @@ export const HEIF: IImage = {
const images: ISize[] = []
let currentOffset = ipcoBox.offset + 8
+ const ipcoEnd = ipcoBox.offset + ipcoBox.size
// Find all ispe and clap boxes
- while (currentOffset < ipcoBox.offset + ipcoBox.size) {
+ while (currentOffset < ipcoEnd) {
const ispeBox = findBox(input, 'ispe', currentOffset)
if (!ispeBox) break
+ if (ispeBox.size < 8) break
+ if (ispeBox.offset + ispeBox.size > ipcoEnd) break
const rawWidth = readUInt32BE(input, ispeBox.offset + 12)
const rawHeight = readUInt32BE(input, ispeBox.offset + 16)
@@ -69,14 +72,16 @@ export const HEIF: IImage = {
const clapBox = findBox(input, 'clap', currentOffset)
let width = rawWidth
let height = rawHeight
- if (clapBox && clapBox.offset < ipcoBox.offset + ipcoBox.size) {
+ if (clapBox && clapBox.offset < ipcoEnd && clapBox.size >= 8) {
const cropRight = readUInt32BE(input, clapBox.offset + 12)
width = rawWidth - cropRight
}
images.push({ height, width })
- currentOffset = ispeBox.offset + ispeBox.size
+ const nextOffset = ispeBox.offset + ispeBox.size
+ if (nextOffset <= currentOffset) break
+ currentOffset = nextOffset
}
if (images.length === 0) {
diff --git a/packages/astro/src/assets/utils/vendor/image-size/types/icns.ts b/packages/astro/src/assets/utils/vendor/image-size/types/icns.ts
index 4c6b1043fe4d..d9076688d3ae 100644
--- a/packages/astro/src/assets/utils/vendor/image-size/types/icns.ts
+++ b/packages/astro/src/assets/utils/vendor/image-size/types/icns.ts
@@ -11,6 +11,7 @@ import { readUInt32BE, toUTF8String } from './utils.js'
*/
const SIZE_HEADER = 4 + 4 // 8
const FILE_LENGTH_OFFSET = 4 // MSB => BIG ENDIAN
+const MIN_ENTRY_LENGTH = 8
/**
* Image Entry
@@ -92,10 +93,20 @@ export const ICNS: IImage = {
const images: ISize[] = []
while (imageOffset < fileLength && imageOffset < inputLength) {
+ if (inputLength - imageOffset < MIN_ENTRY_LENGTH) break
+
const imageHeader = readImageHeader(input, imageOffset)
+ const entryLength = imageHeader[1]
+ if (entryLength < MIN_ENTRY_LENGTH) break
+
const imageSize = getImageSize(imageHeader[0])
- images.push(imageSize)
- imageOffset += imageHeader[1]
+ if (imageSize.width && imageSize.height) {
+ images.push(imageSize)
+ }
+
+ const nextOffset = imageOffset + entryLength
+ if (nextOffset <= imageOffset) break
+ imageOffset = nextOffset
}
if (images.length === 0) {
diff --git a/packages/astro/src/assets/utils/vendor/image-size/types/jp2.ts b/packages/astro/src/assets/utils/vendor/image-size/types/jp2.ts
index 75af18b1f0af..03fed2d1204e 100644
--- a/packages/astro/src/assets/utils/vendor/image-size/types/jp2.ts
+++ b/packages/astro/src/assets/utils/vendor/image-size/types/jp2.ts
@@ -15,8 +15,9 @@ export const JP2: IImage = {
calculate(input) {
const jp2hBox = findBox(input, 'jp2h', 0)
- const ihdrBox = jp2hBox && findBox(input, 'ihdr', jp2hBox.offset + 8)
- if (ihdrBox) {
+ const ihdrBox =
+ jp2hBox && jp2hBox.size >= 8 && findBox(input, 'ihdr', jp2hBox.offset + 8)
+ if (ihdrBox && ihdrBox.size >= 8) {
return {
height: readUInt32BE(input, ihdrBox.offset + 8),
width: readUInt32BE(input, ihdrBox.offset + 12),
diff --git a/packages/astro/src/assets/utils/vendor/image-size/types/jxl.ts b/packages/astro/src/assets/utils/vendor/image-size/types/jxl.ts
index b761f1e3694b..dae1058a5fe8 100644
--- a/packages/astro/src/assets/utils/vendor/image-size/types/jxl.ts
+++ b/packages/astro/src/assets/utils/vendor/image-size/types/jxl.ts
@@ -5,7 +5,7 @@ import { findBox, toUTF8String } from './utils.js'
/** Extracts the codestream from a containerized JPEG XL image */
function extractCodestream(input: Uint8Array): Uint8Array | undefined {
const jxlcBox = findBox(input, 'jxlc', 0)
- if (jxlcBox) {
+ if (jxlcBox && jxlcBox.size >= 8) {
return input.slice(jxlcBox.offset + 8, jxlcBox.offset + jxlcBox.size)
}
@@ -24,10 +24,13 @@ function extractPartialStreams(input: Uint8Array): Uint8Array[] {
while (offset < input.length) {
const jxlpBox = findBox(input, 'jxlp', offset)
if (!jxlpBox) break
+ if (jxlpBox.size < 12) break
partialStreams.push(
input.slice(jxlpBox.offset + 12, jxlpBox.offset + jxlpBox.size),
)
- offset = jxlpBox.offset + jxlpBox.size
+ const nextOffset = jxlpBox.offset + jxlpBox.size
+ if (nextOffset <= offset) break
+ offset = nextOffset
}
return partialStreams
}
diff --git a/packages/astro/src/assets/utils/vendor/image-size/types/utils.ts b/packages/astro/src/assets/utils/vendor/image-size/types/utils.ts
index 9c74975c91a9..a41930963b42 100644
--- a/packages/astro/src/assets/utils/vendor/image-size/types/utils.ts
+++ b/packages/astro/src/assets/utils/vendor/image-size/types/utils.ts
@@ -63,28 +63,42 @@ export function readUInt(
return methods[methodName](input, offset)
}
+const MIN_BOX_HEADER = 8
+
function readBox(input: Uint8Array, offset: number) {
- if (input.length - offset < 4) return
+ if (input.length - offset < MIN_BOX_HEADER) return
const boxSize = readUInt32BE(input, offset)
+
+ // ISO BMFF: size 0 means the box extends to the end of the file
+ if (boxSize === 0) {
+ return {
+ name: toUTF8String(input, offset + 4, offset + 8),
+ offset,
+ size: input.length - offset,
+ }
+ }
+
+ // ISO BMFF: size 1 signals a 64-bit largesize box, which is not supported
+ if (boxSize === 1) return
+ if (boxSize < MIN_BOX_HEADER) return
if (input.length - offset < boxSize) return
+
return {
- name: toUTF8String(input, 4 + offset, 8 + offset),
+ name: toUTF8String(input, offset + 4, offset + 8),
offset,
size: boxSize,
}
}
-export function findBox(
- input: Uint8Array,
- boxName: string,
- currentOffset: number,
-) {
- while (currentOffset < input.length) {
- const box = readBox(input, currentOffset)
+export function findBox(input: Uint8Array, boxName: string, startOffset: number) {
+ let offset = startOffset
+ while (offset < input.length) {
+ const box = readBox(input, offset)
if (!box) break
if (box.name === boxName) return box
- // Fix the infinite loop by ensuring offset always increases
- // If box.size is 0, advance by at least 8 bytes (the size of the box header)
- currentOffset += box.size > 0 ? box.size : 8
+ if (box.size < MIN_BOX_HEADER) break
+ const nextOffset = offset + box.size
+ if (nextOffset <= offset) break
+ offset = nextOffset
}
}
diff --git a/packages/astro/test/units/assets/image-size-malformed.test.ts b/packages/astro/test/units/assets/image-size-malformed.test.ts
new file mode 100644
index 000000000000..2b283177fd8b
--- /dev/null
+++ b/packages/astro/test/units/assets/image-size-malformed.test.ts
@@ -0,0 +1,95 @@
+import * as assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import { describe, it } from 'node:test';
+
+// Must stay a file:// URL: a bare Windows path is rejected by dynamic import as an unknown scheme.
+const lookupUrl = new URL('../../../dist/assets/utils/vendor/image-size/lookup.js', import.meta.url)
+ .href;
+
+const PROBE_TIMEOUT = 10_000;
+
+function writeAscii(buffer: Uint8Array, offset: number, text: string) {
+ for (let i = 0; i < text.length; i++) {
+ buffer[offset + i] = text.charCodeAt(i);
+ }
+}
+
+// A missing guard shows up as a hang, so probe in a child process we can actually kill.
+function probe(payload: Uint8Array) {
+ const source = `
+ const { lookup } = await import(${JSON.stringify(lookupUrl)});
+ const input = new Uint8Array(Buffer.from(process.argv[1], 'base64'));
+ try { lookup(input); } catch {}
+ `;
+ return spawnSync(
+ process.execPath,
+ ['--input-type=module', '-e', source, Buffer.from(payload).toString('base64')],
+ { timeout: PROBE_TIMEOUT, encoding: 'utf8' },
+ );
+}
+
+function assertTerminates(payload: Uint8Array) {
+ const result = probe(payload);
+ assert.equal(result.signal, null, `probe was killed after ${PROBE_TIMEOUT}ms instead of returning`);
+ assert.equal(result.status, 0, `probe exited unexpectedly: ${result.stderr}`);
+}
+
+describe('image-size malformed input', () => {
+ it('terminates on an ICNS entry declaring zero length', () => {
+ const payload = new Uint8Array(24);
+ const view = new DataView(payload.buffer);
+ writeAscii(payload, 0, 'icns');
+ view.setUint32(4, payload.length, false);
+ writeAscii(payload, 8, 'ic09');
+ view.setUint32(12, 0, false);
+
+ assertTerminates(payload);
+ });
+
+ it('terminates on a JXL container with a zero-size jxlp box', () => {
+ const payload = new Uint8Array(44);
+ const view = new DataView(payload.buffer);
+
+ view.setUint32(0, 12, false);
+ writeAscii(payload, 4, 'JXL ');
+ payload.set([0x0d, 0x0a, 0x87, 0x0a], 8);
+
+ view.setUint32(12, 20, false);
+ writeAscii(payload, 16, 'ftyp');
+ writeAscii(payload, 20, 'jxl ');
+ view.setUint32(24, 0, false);
+ writeAscii(payload, 28, 'jxl ');
+
+ view.setUint32(32, 0, false);
+ writeAscii(payload, 36, 'jxlp');
+
+ assertTerminates(payload);
+ });
+
+ it('terminates on a HEIF ipco containing a zero-size ispe box', () => {
+ // Oversized so the ispe dimension reads stay in bounds; a short buffer throws before the loop spins.
+ const payload = new Uint8Array(100);
+ const view = new DataView(payload.buffer);
+
+ view.setUint32(0, 20, false);
+ writeAscii(payload, 4, 'ftyp');
+ writeAscii(payload, 8, 'avif');
+ view.setUint32(12, 0, false);
+ writeAscii(payload, 16, 'avif');
+
+ view.setUint32(20, 80, false);
+ writeAscii(payload, 24, 'meta');
+ view.setUint32(28, 0, false);
+
+ view.setUint32(32, 68, false);
+ writeAscii(payload, 36, 'iprp');
+
+ view.setUint32(40, 60, false);
+ writeAscii(payload, 44, 'ipco');
+
+ view.setUint32(48, 0, false);
+ writeAscii(payload, 52, 'ispe');
+
+ assertTerminates(payload);
+ });
+});
From aeda8105de7a5f15c88654cafdeef5f3c97fde96 Mon Sep 17 00:00:00 2001
From: Erika
Date: Wed, 19 Aug 2026 13:45:57 +0000
Subject: [PATCH 7/8] [ci] format
---
.../astro/test/units/assets/image-size-malformed.test.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/packages/astro/test/units/assets/image-size-malformed.test.ts b/packages/astro/test/units/assets/image-size-malformed.test.ts
index 2b283177fd8b..da7ffa4865ea 100644
--- a/packages/astro/test/units/assets/image-size-malformed.test.ts
+++ b/packages/astro/test/units/assets/image-size-malformed.test.ts
@@ -30,7 +30,11 @@ function probe(payload: Uint8Array) {
function assertTerminates(payload: Uint8Array) {
const result = probe(payload);
- assert.equal(result.signal, null, `probe was killed after ${PROBE_TIMEOUT}ms instead of returning`);
+ assert.equal(
+ result.signal,
+ null,
+ `probe was killed after ${PROBE_TIMEOUT}ms instead of returning`,
+ );
assert.equal(result.status, 0, `probe exited unexpectedly: ${result.stderr}`);
}
From 8a31cba625a23d886614172e1d3b02b8eb896a18 Mon Sep 17 00:00:00 2001
From: "Houston (Bot)" <108291165+astrobot-houston@users.noreply.github.com>
Date: Wed, 19 Aug 2026 07:34:48 -0700
Subject: [PATCH 8/8] [ci] release (#17739)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
.changeset/heavy-moons-repeat.md | 5 --
.changeset/isr-edge-middleware.md | 7 --
.changeset/nine-colts-add.md | 6 --
.changeset/preserve-trailing-slash-html.md | 5 --
.changeset/satteri-0-10-3.md | 6 --
.changeset/social-paws-throw.md | 5 --
examples/advanced-routing/package.json | 4 +-
examples/basics/package.json | 2 +-
examples/blog/package.json | 4 +-
examples/component/package.json | 2 +-
examples/container-with-vitest/package.json | 4 +-
examples/framework-alpine/package.json | 2 +-
examples/framework-multiple/package.json | 6 +-
examples/framework-preact/package.json | 4 +-
examples/framework-react/package.json | 4 +-
examples/framework-solid/package.json | 2 +-
examples/framework-svelte/package.json | 2 +-
examples/framework-vue/package.json | 2 +-
examples/hackernews/package.json | 4 +-
examples/integration/package.json | 2 +-
examples/minimal/package.json | 2 +-
examples/portfolio/package.json | 2 +-
examples/ssr/package.json | 4 +-
examples/starlog/package.json | 2 +-
examples/toolbar-app/package.json | 2 +-
examples/with-markdoc/package.json | 4 +-
examples/with-mdx/package.json | 6 +-
examples/with-nanostores/package.json | 4 +-
examples/with-tailwindcss/package.json | 4 +-
examples/with-vitest/package.json | 2 +-
packages/astro/CHANGELOG.md | 17 +++++
packages/astro/package.json | 2 +-
packages/integrations/cloudflare/CHANGELOG.md | 8 ++
packages/integrations/cloudflare/package.json | 2 +-
packages/integrations/markdoc/CHANGELOG.md | 7 ++
packages/integrations/markdoc/package.json | 2 +-
packages/integrations/mdx/CHANGELOG.md | 10 +++
packages/integrations/mdx/package.json | 2 +-
packages/integrations/netlify/CHANGELOG.md | 8 ++
packages/integrations/netlify/package.json | 2 +-
packages/integrations/node/CHANGELOG.md | 7 ++
packages/integrations/node/package.json | 2 +-
packages/integrations/preact/CHANGELOG.md | 7 ++
packages/integrations/preact/package.json | 2 +-
packages/integrations/react/CHANGELOG.md | 7 ++
packages/integrations/react/package.json | 2 +-
packages/integrations/vercel/CHANGELOG.md | 11 +++
packages/integrations/vercel/package.json | 2 +-
packages/internal-helpers/CHANGELOG.md | 6 ++
packages/internal-helpers/package.json | 2 +-
packages/markdown/remark/CHANGELOG.md | 7 ++
packages/markdown/remark/package.json | 2 +-
packages/markdown/satteri/CHANGELOG.md | 9 +++
packages/markdown/satteri/package.json | 2 +-
pnpm-lock.yaml | 76 +++++++++----------
55 files changed, 192 insertions(+), 122 deletions(-)
delete mode 100644 .changeset/heavy-moons-repeat.md
delete mode 100644 .changeset/isr-edge-middleware.md
delete mode 100644 .changeset/nine-colts-add.md
delete mode 100644 .changeset/preserve-trailing-slash-html.md
delete mode 100644 .changeset/satteri-0-10-3.md
delete mode 100644 .changeset/social-paws-throw.md
diff --git a/.changeset/heavy-moons-repeat.md b/.changeset/heavy-moons-repeat.md
deleted file mode 100644
index ea451b2261df..000000000000
--- a/.changeset/heavy-moons-repeat.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Fixes builds hanging when an image file is malformed
diff --git a/.changeset/isr-edge-middleware.md b/.changeset/isr-edge-middleware.md
deleted file mode 100644
index 308518ac356c..000000000000
--- a/.changeset/isr-edge-middleware.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-'@astrojs/vercel': patch
----
-
-Fixes `middlewareMode: 'edge'` not running your middleware when `isr` is also enabled
-
-Previously, enabling both options deployed the edge middleware but never reached it: requests went straight to the ISR function, which skips rendering entirely on a cache hit. Middleware now runs at the edge for ISR-backed routes before the cached response is served, and query strings are preserved when it forwards the request.
diff --git a/.changeset/nine-colts-add.md b/.changeset/nine-colts-add.md
deleted file mode 100644
index c93fb193a4c2..000000000000
--- a/.changeset/nine-colts-add.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'astro': patch
-'@astrojs/internal-helpers': patch
----
-
-Fixes base path stripping to respect path-segment boundaries. With a configured `base` such as `/docs`, a request like `/docs-archive/page` is no longer treated as being under the base, so routing and `context.url.pathname` now agree on the same pathname.
diff --git a/.changeset/preserve-trailing-slash-html.md b/.changeset/preserve-trailing-slash-html.md
deleted file mode 100644
index ea4023c077d4..000000000000
--- a/.changeset/preserve-trailing-slash-html.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Fixes `astro build` throwing `TypeError: Missing parameter` for dynamic routes when `build.format: 'preserve'` and `trailingSlash: 'always'` are used together. Stripping the framework-injected `.html` suffix dropped the trailing slash that the compiled route pattern requires, so the route no longer matched itself and its params resolved as empty.
diff --git a/.changeset/satteri-0-10-3.md b/.changeset/satteri-0-10-3.md
deleted file mode 100644
index 77fe2bb4e1bd..000000000000
--- a/.changeset/satteri-0-10-3.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-'@astrojs/markdown-satteri': patch
-'@astrojs/mdx': patch
----
-
-Updates Sätteri processor to v0.10.3. See [its changelog](https://github.com/bruits/satteri/blob/main/packages/satteri/CHANGELOG.md#0103--2026-08-19) for details on bugs fixed and features added.
diff --git a/.changeset/social-paws-throw.md b/.changeset/social-paws-throw.md
deleted file mode 100644
index a3ef0b5cc5d8..000000000000
--- a/.changeset/social-paws-throw.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'astro': patch
----
-
-Fixes `Astro.site` always being `undefined` when rendering components via the Container API, even when `site` is set in `astroConfig`
diff --git a/examples/advanced-routing/package.json b/examples/advanced-routing/package.json
index 37f9b60019ac..eedf3c865a21 100644
--- a/examples/advanced-routing/package.json
+++ b/examples/advanced-routing/package.json
@@ -13,8 +13,8 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/node": "^11.1.3",
- "astro": "^7.2.3",
+ "@astrojs/node": "^11.1.4",
+ "astro": "^7.2.4",
"hono": "^4.12.14"
}
}
diff --git a/examples/basics/package.json b/examples/basics/package.json
index 822de066ee9e..deaf8ab28b7a 100644
--- a/examples/basics/package.json
+++ b/examples/basics/package.json
@@ -13,6 +13,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^7.2.3"
+ "astro": "^7.2.4"
}
}
diff --git a/examples/blog/package.json b/examples/blog/package.json
index 39314e1e2e32..c9db9ef3a99b 100644
--- a/examples/blog/package.json
+++ b/examples/blog/package.json
@@ -13,10 +13,10 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/mdx": "^7.0.6",
+ "@astrojs/mdx": "^7.0.7",
"@astrojs/rss": "^4.0.19",
"@astrojs/sitemap": "^3.7.3",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"sharp": "^0.35.0"
}
}
diff --git a/examples/component/package.json b/examples/component/package.json
index 8b3c7a7a72ca..68741a682a52 100644
--- a/examples/component/package.json
+++ b/examples/component/package.json
@@ -18,7 +18,7 @@
],
"scripts": {},
"devDependencies": {
- "astro": "^7.2.3"
+ "astro": "^7.2.4"
},
"peerDependencies": {
"astro": "^5.0.0 || ^6.0.0"
diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json
index 3bcbb4cceca2..17680fc49da6 100644
--- a/examples/container-with-vitest/package.json
+++ b/examples/container-with-vitest/package.json
@@ -14,8 +14,8 @@
"test": "vitest run"
},
"dependencies": {
- "@astrojs/react": "^6.0.3",
- "astro": "^7.2.3",
+ "@astrojs/react": "^6.0.4",
+ "astro": "^7.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"vitest": "^4.1.0"
diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json
index fcf4247b868d..46f29528c737 100644
--- a/examples/framework-alpine/package.json
+++ b/examples/framework-alpine/package.json
@@ -16,6 +16,6 @@
"@astrojs/alpinejs": "^1.0.0",
"@types/alpinejs": "^3.13.11",
"alpinejs": "^3.15.8",
- "astro": "^7.2.3"
+ "astro": "^7.2.4"
}
}
diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json
index c92ab2ce757b..f94b30c0a56a 100644
--- a/examples/framework-multiple/package.json
+++ b/examples/framework-multiple/package.json
@@ -13,14 +13,14 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/preact": "^6.0.3",
- "@astrojs/react": "^6.0.3",
+ "@astrojs/preact": "^6.0.4",
+ "@astrojs/react": "^6.0.4",
"@astrojs/solid-js": "^7.0.2",
"@astrojs/svelte": "^9.0.1",
"@astrojs/vue": "^7.0.2",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"preact": "^10.28.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json
index 270064cf67be..25014ced0441 100644
--- a/examples/framework-preact/package.json
+++ b/examples/framework-preact/package.json
@@ -13,9 +13,9 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/preact": "^6.0.3",
+ "@astrojs/preact": "^6.0.4",
"@preact/signals": "^2.8.1",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"preact": "^10.28.4"
}
}
diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json
index cf8fc47ee87b..1c2024d0c534 100644
--- a/examples/framework-react/package.json
+++ b/examples/framework-react/package.json
@@ -13,10 +13,10 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/react": "^6.0.3",
+ "@astrojs/react": "^6.0.4",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json
index a33a0b3017d3..8826ad707c7b 100644
--- a/examples/framework-solid/package.json
+++ b/examples/framework-solid/package.json
@@ -14,7 +14,7 @@
},
"dependencies": {
"@astrojs/solid-js": "^7.0.2",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"solid-js": "^1.9.11"
}
}
diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json
index 97f67c70989b..86dac97c83f2 100644
--- a/examples/framework-svelte/package.json
+++ b/examples/framework-svelte/package.json
@@ -14,7 +14,7 @@
},
"dependencies": {
"@astrojs/svelte": "^9.0.1",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"svelte": "^5.53.5"
}
}
diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json
index 0915e279dfb6..24fc3c122055 100644
--- a/examples/framework-vue/package.json
+++ b/examples/framework-vue/package.json
@@ -14,7 +14,7 @@
},
"dependencies": {
"@astrojs/vue": "^7.0.2",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"vue": "^3.5.29"
}
}
diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json
index 8f407d843eed..b71a520e7a67 100644
--- a/examples/hackernews/package.json
+++ b/examples/hackernews/package.json
@@ -13,7 +13,7 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/node": "^11.1.3",
- "astro": "^7.2.3"
+ "@astrojs/node": "^11.1.4",
+ "astro": "^7.2.4"
}
}
diff --git a/examples/integration/package.json b/examples/integration/package.json
index 17ada4c6e0c1..a19b0a75a9dc 100644
--- a/examples/integration/package.json
+++ b/examples/integration/package.json
@@ -18,7 +18,7 @@
],
"scripts": {},
"devDependencies": {
- "astro": "^7.2.3"
+ "astro": "^7.2.4"
},
"peerDependencies": {
"astro": "^4.0.0"
diff --git a/examples/minimal/package.json b/examples/minimal/package.json
index 2d741bea1b76..6edf5f8309ab 100644
--- a/examples/minimal/package.json
+++ b/examples/minimal/package.json
@@ -13,6 +13,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^7.2.3"
+ "astro": "^7.2.4"
}
}
diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json
index 4462e8ff4094..dc45d5e2fed4 100644
--- a/examples/portfolio/package.json
+++ b/examples/portfolio/package.json
@@ -13,6 +13,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^7.2.3"
+ "astro": "^7.2.4"
}
}
diff --git a/examples/ssr/package.json b/examples/ssr/package.json
index 250e0d5365f8..a5236ab64c87 100644
--- a/examples/ssr/package.json
+++ b/examples/ssr/package.json
@@ -14,9 +14,9 @@
"server": "node dist/server/entry.mjs"
},
"dependencies": {
- "@astrojs/node": "^11.1.3",
+ "@astrojs/node": "^11.1.4",
"@astrojs/svelte": "^9.0.1",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"svelte": "^5.53.5"
}
}
diff --git a/examples/starlog/package.json b/examples/starlog/package.json
index 663a07f86914..3b465f28f07a 100644
--- a/examples/starlog/package.json
+++ b/examples/starlog/package.json
@@ -9,7 +9,7 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"sass": "^1.97.3",
"sharp": "^0.35.0"
},
diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json
index 0dafd9e5c889..973a93eb5d8e 100644
--- a/examples/toolbar-app/package.json
+++ b/examples/toolbar-app/package.json
@@ -16,7 +16,7 @@
},
"devDependencies": {
"@types/node": "^22.10.6",
- "astro": "^7.2.3"
+ "astro": "^7.2.4"
},
"engines": {
"node": ">=22.12.0"
diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json
index a53323d1f17f..f46d24e068ca 100644
--- a/examples/with-markdoc/package.json
+++ b/examples/with-markdoc/package.json
@@ -13,7 +13,7 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/markdoc": "^2.0.7",
- "astro": "^7.2.3"
+ "@astrojs/markdoc": "^2.0.8",
+ "astro": "^7.2.4"
}
}
diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json
index 840c85e2373f..e8a4b9953690 100644
--- a/examples/with-mdx/package.json
+++ b/examples/with-mdx/package.json
@@ -13,9 +13,9 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/mdx": "^7.0.6",
- "@astrojs/preact": "^6.0.3",
- "astro": "^7.2.3",
+ "@astrojs/mdx": "^7.0.7",
+ "@astrojs/preact": "^6.0.4",
+ "astro": "^7.2.4",
"preact": "^10.28.4"
}
}
diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json
index 32216f6f8185..a7b840f2a2de 100644
--- a/examples/with-nanostores/package.json
+++ b/examples/with-nanostores/package.json
@@ -13,9 +13,9 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/preact": "^6.0.3",
+ "@astrojs/preact": "^6.0.4",
"@nanostores/preact": "^1.0.0",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"nanostores": "^1.1.1",
"preact": "^10.28.4"
}
diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json
index 312853384bb7..b0dcc0e8d279 100644
--- a/examples/with-tailwindcss/package.json
+++ b/examples/with-tailwindcss/package.json
@@ -13,10 +13,10 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/mdx": "^7.0.6",
+ "@astrojs/mdx": "^7.0.7",
"@tailwindcss/vite": "^4.2.1",
"@types/canvas-confetti": "^1.9.0",
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"canvas-confetti": "^1.9.4",
"tailwindcss": "^4.2.1",
"vite": "^8.0.13"
diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json
index 3f555c2e0d23..a03ed9530be8 100644
--- a/examples/with-vitest/package.json
+++ b/examples/with-vitest/package.json
@@ -14,7 +14,7 @@
"test": "vitest"
},
"dependencies": {
- "astro": "^7.2.3",
+ "astro": "^7.2.4",
"vitest": "^5.0.0-beta.2"
}
}
diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md
index 9450cedefd61..39f99190bd78 100644
--- a/packages/astro/CHANGELOG.md
+++ b/packages/astro/CHANGELOG.md
@@ -1,5 +1,22 @@
# astro
+## 7.2.4
+
+### Patch Changes
+
+- [#17747](https://github.com/withastro/astro/pull/17747) [`a90ff66`](https://github.com/withastro/astro/commit/a90ff6650fc244bf6ec86844a82dd13b88a4fd41) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes builds hanging when an image file is malformed
+
+- [#17701](https://github.com/withastro/astro/pull/17701) [`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527) Thanks [@matthewp](https://github.com/matthewp)! - Fixes base path stripping to respect path-segment boundaries. With a configured `base` such as `/docs`, a request like `/docs-archive/page` is no longer treated as being under the base, so routing and `context.url.pathname` now agree on the same pathname.
+
+- [#17742](https://github.com/withastro/astro/pull/17742) [`70b449d`](https://github.com/withastro/astro/commit/70b449ddbae465348aa9337ff7d1d62ad8286e1a) Thanks [@Kjubikstronk](https://github.com/Kjubikstronk)! - Fixes `astro build` throwing `TypeError: Missing parameter` for dynamic routes when `build.format: 'preserve'` and `trailingSlash: 'always'` are used together. Stripping the framework-injected `.html` suffix dropped the trailing slash that the compiled route pattern requires, so the route no longer matched itself and its params resolved as empty.
+
+- [#17703](https://github.com/withastro/astro/pull/17703) [`771b0a9`](https://github.com/withastro/astro/commit/771b0a9a04ca4b4bcdd098b2062a5903cc988112) Thanks [@astrobot-houston](https://github.com/astrobot-houston)! - Fixes `Astro.site` always being `undefined` when rendering components via the Container API, even when `site` is set in `astroConfig`
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527), [`bc171af`](https://github.com/withastro/astro/commit/bc171af0e29a1bb4ca56beffde1c4c03e1bb227f)]:
+ - @astrojs/internal-helpers@0.10.4
+ - @astrojs/markdown-satteri@0.3.7
+ - @astrojs/markdown-remark@7.2.4
+
## 7.2.3
### Patch Changes
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 254f0152ba98..f7bfb10b347c 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -1,6 +1,6 @@
{
"name": "astro",
- "version": "7.2.3",
+ "version": "7.2.4",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module",
"author": "withastro",
diff --git a/packages/integrations/cloudflare/CHANGELOG.md b/packages/integrations/cloudflare/CHANGELOG.md
index edf1647ee76c..19d156646a5e 100644
--- a/packages/integrations/cloudflare/CHANGELOG.md
+++ b/packages/integrations/cloudflare/CHANGELOG.md
@@ -1,5 +1,13 @@
# @astrojs/cloudflare
+## 14.2.3
+
+### Patch Changes
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+ - @astrojs/underscore-redirects@1.0.4
+
## 14.2.2
### Patch Changes
diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json
index c3d3b2bdddf1..774ef12a332c 100644
--- a/packages/integrations/cloudflare/package.json
+++ b/packages/integrations/cloudflare/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/cloudflare",
"description": "Deploy your site to Cloudflare Workers",
- "version": "14.2.2",
+ "version": "14.2.3",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/integrations/markdoc/CHANGELOG.md b/packages/integrations/markdoc/CHANGELOG.md
index 708266a66d9d..a90f2a693fd3 100644
--- a/packages/integrations/markdoc/CHANGELOG.md
+++ b/packages/integrations/markdoc/CHANGELOG.md
@@ -1,5 +1,12 @@
# @astrojs/markdoc
+## 2.0.8
+
+### Patch Changes
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+
## 2.0.7
### Patch Changes
diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json
index 44cb2ff92818..073339796d7a 100644
--- a/packages/integrations/markdoc/package.json
+++ b/packages/integrations/markdoc/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/markdoc",
"description": "Add support for Markdoc in your Astro site",
- "version": "2.0.7",
+ "version": "2.0.8",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/integrations/mdx/CHANGELOG.md b/packages/integrations/mdx/CHANGELOG.md
index ca3822b05bc3..c67c936ef938 100644
--- a/packages/integrations/mdx/CHANGELOG.md
+++ b/packages/integrations/mdx/CHANGELOG.md
@@ -1,5 +1,15 @@
# @astrojs/mdx
+## 7.0.7
+
+### Patch Changes
+
+- [#17731](https://github.com/withastro/astro/pull/17731) [`bc171af`](https://github.com/withastro/astro/commit/bc171af0e29a1bb4ca56beffde1c4c03e1bb227f) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updates Sätteri processor to v0.10.3. See [its changelog](https://github.com/bruits/satteri/blob/main/packages/satteri/CHANGELOG.md#0103--2026-08-19) for details on bugs fixed and features added.
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+ - @astrojs/markdown-remark@7.2.4
+
## 7.0.6
### Patch Changes
diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json
index 4713e5c3d9bd..08121efe9318 100644
--- a/packages/integrations/mdx/package.json
+++ b/packages/integrations/mdx/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/mdx",
"description": "Add support for MDX pages in your Astro site",
- "version": "7.0.6",
+ "version": "7.0.7",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/integrations/netlify/CHANGELOG.md b/packages/integrations/netlify/CHANGELOG.md
index eada18cb9193..26e314000eb3 100644
--- a/packages/integrations/netlify/CHANGELOG.md
+++ b/packages/integrations/netlify/CHANGELOG.md
@@ -1,5 +1,13 @@
# @astrojs/netlify
+## 8.2.3
+
+### Patch Changes
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+ - @astrojs/underscore-redirects@1.0.4
+
## 8.2.2
### Patch Changes
diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json
index 079fa3deb609..76fa0759b611 100644
--- a/packages/integrations/netlify/package.json
+++ b/packages/integrations/netlify/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/netlify",
"description": "Deploy your site to Netlify",
- "version": "8.2.2",
+ "version": "8.2.3",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md
index 904f6ce7a729..514db70e8ee0 100644
--- a/packages/integrations/node/CHANGELOG.md
+++ b/packages/integrations/node/CHANGELOG.md
@@ -1,5 +1,12 @@
# @astrojs/node
+## 11.1.4
+
+### Patch Changes
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+
## 11.1.3
### Patch Changes
diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json
index 88b1ec478160..ae99dfab80f5 100644
--- a/packages/integrations/node/package.json
+++ b/packages/integrations/node/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/node",
"description": "Deploy your site to a Node.js server",
- "version": "11.1.3",
+ "version": "11.1.4",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/integrations/preact/CHANGELOG.md b/packages/integrations/preact/CHANGELOG.md
index 3ea4cb82c890..088ccc09e494 100644
--- a/packages/integrations/preact/CHANGELOG.md
+++ b/packages/integrations/preact/CHANGELOG.md
@@ -1,5 +1,12 @@
# @astrojs/preact
+## 6.0.4
+
+### Patch Changes
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+
## 6.0.3
### Patch Changes
diff --git a/packages/integrations/preact/package.json b/packages/integrations/preact/package.json
index 0c772b8415a0..242d026cf7b2 100644
--- a/packages/integrations/preact/package.json
+++ b/packages/integrations/preact/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/preact",
"description": "Use Preact components within Astro",
- "version": "6.0.3",
+ "version": "6.0.4",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/integrations/react/CHANGELOG.md b/packages/integrations/react/CHANGELOG.md
index 03144c4229cb..11fa1a479d0c 100644
--- a/packages/integrations/react/CHANGELOG.md
+++ b/packages/integrations/react/CHANGELOG.md
@@ -1,5 +1,12 @@
# @astrojs/react
+## 6.0.4
+
+### Patch Changes
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+
## 6.0.3
### Patch Changes
diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json
index 4f9a4cb88997..af7b3d54365d 100644
--- a/packages/integrations/react/package.json
+++ b/packages/integrations/react/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/react",
"description": "Use React components within Astro",
- "version": "6.0.3",
+ "version": "6.0.4",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/integrations/vercel/CHANGELOG.md b/packages/integrations/vercel/CHANGELOG.md
index 9dc575684820..8569bc7edde5 100644
--- a/packages/integrations/vercel/CHANGELOG.md
+++ b/packages/integrations/vercel/CHANGELOG.md
@@ -1,5 +1,16 @@
# @astrojs/vercel
+## 11.0.7
+
+### Patch Changes
+
+- [#17687](https://github.com/withastro/astro/pull/17687) [`0a22ff5`](https://github.com/withastro/astro/commit/0a22ff5b7e4600356ccabfe571b7ffdad76064d7) Thanks [@asmyshlyaev177](https://github.com/asmyshlyaev177)! - Fixes `middlewareMode: 'edge'` not running your middleware when `isr` is also enabled
+
+ Previously, enabling both options deployed the edge middleware but never reached it: requests went straight to the ISR function, which skips rendering entirely on a cache hit. Middleware now runs at the edge for ISR-backed routes before the cached response is served, and query strings are preserved when it forwards the request.
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+
## 11.0.6
### Patch Changes
diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json
index 77293e389831..7fe1bb134717 100644
--- a/packages/integrations/vercel/package.json
+++ b/packages/integrations/vercel/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/vercel",
"description": "Deploy your site to Vercel",
- "version": "11.0.6",
+ "version": "11.0.7",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/internal-helpers/CHANGELOG.md b/packages/internal-helpers/CHANGELOG.md
index 1d64efb40a7a..591766598b54 100644
--- a/packages/internal-helpers/CHANGELOG.md
+++ b/packages/internal-helpers/CHANGELOG.md
@@ -1,5 +1,11 @@
# @astrojs/internal-helpers
+## 0.10.4
+
+### Patch Changes
+
+- [#17701](https://github.com/withastro/astro/pull/17701) [`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527) Thanks [@matthewp](https://github.com/matthewp)! - Fixes base path stripping to respect path-segment boundaries. With a configured `base` such as `/docs`, a request like `/docs-archive/page` is no longer treated as being under the base, so routing and `context.url.pathname` now agree on the same pathname.
+
## 0.10.3
### Patch Changes
diff --git a/packages/internal-helpers/package.json b/packages/internal-helpers/package.json
index bbdb87c4faa3..5297becb99fe 100644
--- a/packages/internal-helpers/package.json
+++ b/packages/internal-helpers/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/internal-helpers",
"description": "Internal helpers used by core Astro packages.",
- "version": "0.10.3",
+ "version": "0.10.4",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/markdown/remark/CHANGELOG.md b/packages/markdown/remark/CHANGELOG.md
index 351bf1cfe69a..c7bb7c37486c 100644
--- a/packages/markdown/remark/CHANGELOG.md
+++ b/packages/markdown/remark/CHANGELOG.md
@@ -1,5 +1,12 @@
# @astrojs/markdown-remark
+## 7.2.4
+
+### Patch Changes
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+
## 7.2.3
### Patch Changes
diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json
index 3326573f6146..a3da1d2d47c1 100644
--- a/packages/markdown/remark/package.json
+++ b/packages/markdown/remark/package.json
@@ -1,6 +1,6 @@
{
"name": "@astrojs/markdown-remark",
- "version": "7.2.3",
+ "version": "7.2.4",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/packages/markdown/satteri/CHANGELOG.md b/packages/markdown/satteri/CHANGELOG.md
index 37ddf3a4bede..93bce8dfd973 100644
--- a/packages/markdown/satteri/CHANGELOG.md
+++ b/packages/markdown/satteri/CHANGELOG.md
@@ -1,5 +1,14 @@
# @astrojs/markdown-satteri
+## 0.3.7
+
+### Patch Changes
+
+- [#17731](https://github.com/withastro/astro/pull/17731) [`bc171af`](https://github.com/withastro/astro/commit/bc171af0e29a1bb4ca56beffde1c4c03e1bb227f) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updates Sätteri processor to v0.10.3. See [its changelog](https://github.com/bruits/satteri/blob/main/packages/satteri/CHANGELOG.md#0103--2026-08-19) for details on bugs fixed and features added.
+
+- Updated dependencies [[`05763a0`](https://github.com/withastro/astro/commit/05763a0884aabb1da78a2749d5bb9d41ae620527)]:
+ - @astrojs/internal-helpers@0.10.4
+
## 0.3.6
### Patch Changes
diff --git a/packages/markdown/satteri/package.json b/packages/markdown/satteri/package.json
index 133ea380c5f2..8d67782bcefd 100644
--- a/packages/markdown/satteri/package.json
+++ b/packages/markdown/satteri/package.json
@@ -1,6 +1,6 @@
{
"name": "@astrojs/markdown-satteri",
- "version": "0.3.6",
+ "version": "0.3.7",
"type": "module",
"author": "withastro",
"license": "MIT",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 90314c03aa58..a877f81b1274 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -388,10 +388,10 @@ importers:
examples/advanced-routing:
dependencies:
'@astrojs/node':
- specifier: ^11.1.3
+ specifier: ^11.1.4
version: link:../../packages/integrations/node
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
hono:
specifier: ^4.12.14
@@ -400,13 +400,13 @@ importers:
examples/basics:
dependencies:
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/blog:
dependencies:
'@astrojs/mdx':
- specifier: ^7.0.6
+ specifier: ^7.0.7
version: link:../../packages/integrations/mdx
'@astrojs/rss':
specifier: ^4.0.19
@@ -415,7 +415,7 @@ importers:
specifier: ^3.7.3
version: link:../../packages/integrations/sitemap
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
sharp:
specifier: ^0.35.0
@@ -424,16 +424,16 @@ importers:
examples/component:
devDependencies:
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/container-with-vitest:
dependencies:
'@astrojs/react':
- specifier: ^6.0.3
+ specifier: ^6.0.4
version: link:../../packages/integrations/react
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
react:
specifier: ^18.3.1
@@ -464,16 +464,16 @@ importers:
specifier: ^3.15.8
version: 3.15.8
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/framework-multiple:
dependencies:
'@astrojs/preact':
- specifier: ^6.0.3
+ specifier: ^6.0.4
version: link:../../packages/integrations/preact
'@astrojs/react':
- specifier: ^6.0.3
+ specifier: ^6.0.4
version: link:../../packages/integrations/react
'@astrojs/solid-js':
specifier: ^7.0.2
@@ -491,7 +491,7 @@ importers:
specifier: ^18.3.7
version: 18.3.7(@types/react@18.3.28)
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
preact:
specifier: ^10.28.4
@@ -515,13 +515,13 @@ importers:
examples/framework-preact:
dependencies:
'@astrojs/preact':
- specifier: ^6.0.3
+ specifier: ^6.0.4
version: link:../../packages/integrations/preact
'@preact/signals':
specifier: ^2.8.1
version: 2.8.2(preact@10.29.0)
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
preact:
specifier: ^10.28.4
@@ -530,7 +530,7 @@ importers:
examples/framework-react:
dependencies:
'@astrojs/react':
- specifier: ^6.0.3
+ specifier: ^6.0.4
version: link:../../packages/integrations/react
'@types/react':
specifier: ^18.3.28
@@ -539,7 +539,7 @@ importers:
specifier: ^18.3.7
version: 18.3.7(@types/react@18.3.28)
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
react:
specifier: ^18.3.1
@@ -554,7 +554,7 @@ importers:
specifier: ^7.0.2
version: link:../../packages/integrations/solid
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
solid-js:
specifier: ^1.9.11
@@ -566,7 +566,7 @@ importers:
specifier: ^9.0.1
version: link:../../packages/integrations/svelte
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
svelte:
specifier: ^5.53.5
@@ -578,7 +578,7 @@ importers:
specifier: ^7.0.2
version: link:../../packages/integrations/vue
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
vue:
specifier: ^3.5.29
@@ -587,40 +587,40 @@ importers:
examples/hackernews:
dependencies:
'@astrojs/node':
- specifier: ^11.1.3
+ specifier: ^11.1.4
version: link:../../packages/integrations/node
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/integration:
devDependencies:
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/minimal:
dependencies:
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/portfolio:
dependencies:
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/ssr:
dependencies:
'@astrojs/node':
- specifier: ^11.1.3
+ specifier: ^11.1.4
version: link:../../packages/integrations/node
'@astrojs/svelte':
specifier: ^9.0.1
version: link:../../packages/integrations/svelte
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
svelte:
specifier: ^5.53.5
@@ -629,7 +629,7 @@ importers:
examples/starlog:
dependencies:
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
sass:
specifier: ^1.97.3
@@ -644,28 +644,28 @@ importers:
specifier: ^22.19.0
version: 22.19.19
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/with-markdoc:
dependencies:
'@astrojs/markdoc':
- specifier: ^2.0.7
+ specifier: ^2.0.8
version: link:../../packages/integrations/markdoc
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
examples/with-mdx:
dependencies:
'@astrojs/mdx':
- specifier: ^7.0.6
+ specifier: ^7.0.7
version: link:../../packages/integrations/mdx
'@astrojs/preact':
- specifier: ^6.0.3
+ specifier: ^6.0.4
version: link:../../packages/integrations/preact
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
preact:
specifier: ^10.28.4
@@ -674,13 +674,13 @@ importers:
examples/with-nanostores:
dependencies:
'@astrojs/preact':
- specifier: ^6.0.3
+ specifier: ^6.0.4
version: link:../../packages/integrations/preact
'@nanostores/preact':
specifier: ^1.0.0
version: 1.0.0(nanostores@1.1.1)(preact@10.29.0)
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
nanostores:
specifier: ^1.1.1
@@ -692,7 +692,7 @@ importers:
examples/with-tailwindcss:
dependencies:
'@astrojs/mdx':
- specifier: ^7.0.6
+ specifier: ^7.0.7
version: link:../../packages/integrations/mdx
'@tailwindcss/vite':
specifier: ^4.2.1
@@ -701,7 +701,7 @@ importers:
specifier: ^1.9.0
version: 1.9.0
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
canvas-confetti:
specifier: ^1.9.4
@@ -716,7 +716,7 @@ importers:
examples/with-vitest:
dependencies:
astro:
- specifier: ^7.2.3
+ specifier: ^7.2.4
version: link:../../packages/astro
vitest:
specifier: ^5.0.0-beta.2