AstroEco is Contributing…
Display your GitHub pull requests using astro-loader-github-prs

Closes #17682
Changes
Astro.siteis now correctly set when rendering components via the Container API. Previously,astroConfig.sitepassed toAstroContainer.create()was accepted in the type signature but never read — the value was never forwarded to the internalcreateManifest()call or written onto theSSRManifest, soAstro.sitewas alwaysundefined.- Wires
astroConfig.sitethroughAstroContainer.create()→ constructor →createManifest(), and adds'site'to theAstroContainerManifestPick type so a pre-built manifest can also carry the value.
Testing
- Added
'Astro.site reflects astroConfig.site'— verifies thatAstro.sitematches the URL set inastroConfig.site. - Added
'Astro.site is undefined when astroConfig.site is not set'— verifies the default behavior remains unchanged.
Docs
No docs update needed — AstroContainer.create() already documents the astroConfig.site option; this fix makes it work as documented.

Changes
- Base stripping now only removes a configured
basewhen the pathname is the base itself or continues at a path-segment boundary. Withbase: '/docs', a request like/docs-archive/pageis treated as outside the base instead of being rewritten to/page. This keeps route matching andcontext.url.pathnamein agreement. - Consolidates the three duplicated base-stripping implementations (
BaseApp.removeBase,FetchState.#computePathname, and the i18n domain helper) into a single sharedstripRequestBasehelper in@astrojs/internal-helpers, matching the boundary logic the router already uses instripBase.
Testing
- Adds
base-prefix-boundary.test.tscovering single-character extensions of the base prefix (/docsX/...,/docs2/...,/docs-/...), asserting they do not resolve to a route under the base.
Docs
- No docs update needed; this corrects internal pathname handling with no public API change.

Changes
computePreferredLocaleListcompared object-form locale codes exactly, while every other locale comparison ini18n/utils.tsnormalizes both sides first. That one raw comparison is now normalized like the rest.- The result is a self-contradiction on a single request:
sortAndFilterLocalesfilters on normalized codes, so a locale configured as{ path: 'english', codes: ['en-us'] }passes the filter when a browser sendsAccept-Language: en-US, and is then silently dropped by the exact comparison.Astro.preferredLocalereturns'en-us'whileAstro.preferredLocaleListreturns[]. - The two branches of the same loop disagreed: the string-locale branch already normalized (so
locales: ['en-us']works today), the object-form branch did not. Underscore codes such asen_USwere affected the same way, sincenormalizeTheLocalemaps_to-. - The configured casing is still what gets returned — the comparison is normalized, but the original
codeis what's pushed, matching howgetLocaleByPathcompares normalized and returns the configured value.
Production change is one line:
- if (code === browserLocale.locale) {
+ if (normalizeTheLocale(code) === normalizeTheLocale(browserLocale.locale)) {One thing I deliberately left out: computePreferredLocale breaks out of its codes loop on first match (#16600), and this list function doesn't. That's a visible behaviour difference between the pair, but it's a separate change with its own semantics, so I'd rather ask than bundle it — happy to follow up if you want them aligned.
Testing
Added to the existing packages/astro/test/units/i18n/i18n-utils.test.ts, in the style of the surrounding cases:
- object-form codes matched case-insensitively (
codes: ['en-us']vsen-US) — failed before this change, returning[] - object-form codes with an underscore separator (
codes: ['en_US']) — failed before, returning[] - object-form matches sorted by quality value — failed before
- the configured casing is returned for an exact-case object-form code
- string locales still match case-insensitively
- a code is returned rather than the path for a multi-code entry
The last three pass both before and after, so they guard against a regression rather than describing the fix.
Locally: 32/32 in that file, and the full i18n unit suite (396 tests), tsc -b, format:ci, biome and eslint all pass. One caveat — astro-scripts test fails on my machine with ERR_UNKNOWN_FILE_EXTENSION for .ts on Node 22.17.1 (the repo's .nvmrc is 24.14.0); it fails identically on files I didn't touch, so I ran the units via the equivalent node --experimental-strip-types --test. CI on Node 24 will run them through the wrapper.
Docs
No docs change needed. Astro.preferredLocaleList is already documented as returning the list of matching locales; this makes the object-form config shape behave as documented, rather than changing any documented behaviour.

Changes
- The dep-scan plugins for
.astrofiles rewrite top-levelreturntothrowso esbuild/Rolldown accept the frontmatter as an ES module. They found thosereturnkeywords with a single regex whose skip group covered strings, template literals and comments, but not regex literals. A quote inside a regex literal, as invalue.replace(/"/g, """), was then read as an opening string delimiter, so quote pairing stayed off by one for the rest of the frontmatter and every later top-levelreturnwas left alone. Vite reportedFailed to run dependency scanplus oneTop-level return cannot be used inside an ECMAScript moduleper survivingreturn, and only on a coldnode_modules/.vite, which made it look intermittent. replaceTopLevelReturnsnow scans the frontmatter character by character instead. It skips strings, template literals, line and block comments, and regex literals, and only rewrites areturnfound in code. A/is treated as the start of a regex literal only when the preceding token allows an expression there, so division is left alone.- Moved the helper to
src/utils/frontmatter.ts, since the esbuild and Rolldown plugins each carried their own copy of the same tokenizer and the same bug.
Closes #17697
Testing
Added test/frontmatter-returns.test.ts covering the reported case along with division, character classes, member access, strings and comments. The regex-literal case fails on main and passes here. The existing top-level-return integration test still passes.
Docs
No docs needed, this only fixes the dependency scan for frontmatter that was already valid.

Changes
- Shallow-clones
langAliasbefore passing it to Shiki'screateHighlighter()inpackages/internal-helpers/src/shiki.ts. Shiki'sRegistry.loadLanguage()writes built-in language aliases (e.g.js,cjs,mjsfor JavaScript) directly into thelangAliasobject it receives. Because Astro passed the same object reference from the resolved config, those aliases leaked back intoconfig.markdown.shikiConfig.langAlias. SincecomputeConfigHash()runs after the Vite build, the hash then reflected which languages appeared in code blocks rather than the user's actual config, causing the incremental build cache to be invalidated on every content change.
Closes #17693
Testing
- Adds
packages/internal-helpers/test/shiki.test.tswith a test that highlights a JavaScript code block and asserts the originallangAliasobject passed tocreateShikiHighlighteris not mutated.
Docs
- No docs update needed; this is an internal bug fix with no user-facing API change.

Changes
- Require a directory boundary when deciding whether an MDX file is under
src/pages. - Prevent sibling directories such as
src/pages-oldandsrc/pages2from receiving page-only automatic charset injection in both the unified and Sätteri processors.
Testing
- Added focused coverage for real pages and similarly prefixed sibling directories across both processor paths.
- Ran:
pnpm -C packages/integrations/mdx test - Ran:
pnpm -C packages/integrations/mdx build - Ran:
pnpm lint:ai
Docs
No docs changes. This corrects internal page classification without changing the public API.

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
astro@7.2.3
Patch Changes
-
#17636
51723b1Thanks @matthewp! - Fixes the dev server sometimes matching against stale routes after pages were added, removed, or renamed, requiring a dev server restart to pick up the change -
#17636
51723b1Thanks @matthewp! - Fixes the composable request helpers (astro/fetch) throwing an error when used on a request that had been rewritten withAstro.rewrite()ornext() -
#17636
51723b1Thanks @matthewp! - Refactors Astro's internal server-side request handling. This is an internal change: all documented public APIs, includingAppandNodeApp, keep their existing signatures and behavior.The undocumented internal
app.pipelineproperty and theAppPipelineexport fromastro/apphave been removed. Adapters that usedapp.pipeline.getLogger()to wait for the configured log destination can call the newapp.getLogger()instead.As a result of this refactor,
new FetchState(request)fromastro/fetchnow works anywhere inside a built Astro server — including customsrc/fetch.tsentrypoints — without the request needing to first pass throughapp.render(). Previously this threw an error, breaking patterns like the Cloudflare adapter's advanced custom-worker setup. -
#17572
2066f39Thanks @matthewp! - Fixes a crash when a request arrives with a malformed port in theHostheader (for exampleexample.com:65536orexample.com:8080:8080). Such a host made the constructed request URL invalid, and the fallback that was meant to recover reused the same invalid host and threw again. The request URL now degrades to a host the server controls when the incoming host cannot be parsed, so the request is handled instead of erroring. -
#17685
9f15609Thanks @astrobot-houston! - Fixes a dev server error where an SSR full reload triggered by a third-party Vite plugin (such as@tailwindcss/vite) could fail withFailed to load url astro:server-app.js -
#17636
51723b1Thanks @matthewp! - Improves error handling for custom log destinations. When the configured logger fails to load, Astro now reports the error and continues with the default console logger instead of failing the first request. -
#17631
cf29becThanks @matthewp! - FixesgetCollection()andgetEntry()throwingDataCloneErrorwhen a collection schema transform returns aTemporal.PlainDateor other class instance. -
Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
- @astrojs/markdown-remark@7.2.3
- @astrojs/markdown-satteri@0.3.6
@astrojs/cloudflare@14.2.2
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
- @astrojs/underscore-redirects@1.0.4
@astrojs/markdoc@2.0.7
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
@astrojs/mdx@7.0.6
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
- @astrojs/markdown-remark@7.2.3
@astrojs/netlify@8.2.2
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
- @astrojs/underscore-redirects@1.0.4
@astrojs/node@11.1.3
Patch Changes
-
#17636
51723b1Thanks @matthewp! - Updates the adapter to wait for the configured log destination through Astro's newapp.getLogger()API. This release requires Astro 7.2.1 or later. -
Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
@astrojs/preact@6.0.3
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
@astrojs/react@6.0.3
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
@astrojs/vercel@11.0.6
Patch Changes
-
#17680
ce9f1daThanks @astrobot-houston! - Fixes server islands returning 404 responses in Vercel deployments usingoutput: "static" -
Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
@astrojs/internal-helpers@0.10.3
Patch Changes
- #17696
8c193f6Thanks @astrobot-houston! - Fixes incremental build cache invalidation caused by Shiki mutating thelangAliasconfig object when loading languages
@astrojs/ts-plugin@1.10.11
Patch Changes
- #17668
bef9db5Thanks @lazerg! - Fixes Astro's ambient types leaking into unrelated TypeScript projects. In a monorepo with hoistednode_modules, the plugin found the sharedastroinstall from any project and injectedenv.d.tsandastro-jsx.d.tsinto it, which pulled@types/nodeinto projects that never asked for it. The plugin now only injects those types when the project actually depends onastroor has anastro.config.*file.
@astrojs/markdown-remark@7.2.3
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3
@astrojs/markdown-satteri@0.3.6
Patch Changes
- Updated dependencies [
8c193f6]:- @astrojs/internal-helpers@0.10.3

Changes
- Require a directory boundary when checking whether a file is inside
src/pages. - Prevent sibling directories such as
src/pages-oldandsrc/pages2from receiving page-only handling. - Add regression coverage for pages, nested pages, endpoints, and sibling-directory false positives.
Testing
pnpm --filter astro exec astro-scripts test "test/units/util/*.test.ts" --strip-types(41 passed)pnpm --filter astro buildpnpm lint:ai
Docs
No docs changes. This fixes internal path classification without changing the public API.
Description
We noticed while working on #4121 that our current size limit checks won’t reflect changes to the page sidebars because we test against the start template’s landing page.
This PR adds one of the example guide pages in the starter template to our size limit checks so we also track changes in size to the more standard documentation page layouts as suggested by @trueberryless.
While there I’ve also slightly reduced the size limit for the HTML pages to be a bit closer to the current output sizes and trigger a failure earlier if those increase.

Changes
- Updates the dependency diffing action to 1.7.1. Primarily to fix a bug for PRs from branches that were behind
mainwhere they would report changes that were inaccurate as you can see in #17688 (comment) for example. The issue should be fixed by e18e/action-dependency-diff#177 which is included in the 1.7.1 release. - Versions 1.6 and 1.7 updated internal dependencies, improved trusted publishing checks, and fixed duplicate scanning (a feature we don’t use) so otherwise should be a safe update.
Testing
Tested upstream, 🤞
Docs
n/a
Description
This PR refactors Starlight’s mobile menu toggle to use the Popover API instead of the current JS-powered <button> with aria-expanded pattern.
-
A small amount of JS remains but it’s now only a progressive enhancement for focus trapping. The core functionality of opening/closing the mobile menu and locking body scroll works even if JS breaks for whatever reason.
-
The JS is now in the
PageFramecomponent as a custom element that attaches to the popover body instead of the menu button. It reacts to global events (matchMedia()) and the popover’s"toggle"event so it makes more sense to live alongside the element it attaches to. This also means the menu button component is now easier to override as any button with the correctpopovertargetattribute will work. -
CSS switches from hooking into
[aria-expanded]to using the:popover-openpseudo class. -
We no longer add a
data-mobile-menu-expandedattribute to<body>when the menu is open and usebody:has(#starlight__sidebar:popover-open)in CSS instead, which works without the JS-managed attribute. -
While I was updating the button, I switched from
aria-labelto a visually hidden span of text, which is generally the recommended pattern for labels where possible.
Browser compatibility
Use of the Popover API requires slightly bumping our minimum supported browsers:
- Firefox 121 (December 2023) => 125 (April 2024)
- Chromium 111 (March 2023) => 116 (August 2023)
- Safari 16.4 (March 2023) => 17.0 (September 2023) (for both macOS and iOS)
According to browsersl.ist, comparing before and after shows a drop in global coverage of 1 percentage point. The newest minimum supported browser will be Firefox 125, released 29 months ago.
N.B. that umbrella compatibility measures such as wf-popover show support only arriving later in some browsers. However, IIUC this is due to subfeatures that we are not relying on not being ready yet and the features we require are safe even in these older browser versions:
Safari on iOS had a long-standing bug in versions 17.0–18.2 which prevented tap-away clicks closing popovers as is expected. However, we do not require that behaviour in Starlight mobile menus as there is nowhere a user expects to click away to and the main control is the button.
Accessibility
The button with popovertarget and popover combo has built-in accessible roles equivalent to our previous aria-expanded pattern. I tested with VoiceOver in Firefox, Chrome, and Safari on macOS 26.5.1 and found the behaviour to be a very slight improvement in my opinion compared to the current announcements although both are basically equivalent. Would be great to test in more scenarios!
Demo
Here’s a small screen capture of the live Starlight docs and compared to this branch, showing the menu working without JavaScript after these changes:
popover.mp4

Changes
- No code changes, just tests
Testing
- Uses a local mock rather than going to GitHub, to prevent flakiness caused by GitHub not responding in time.
Docs
N/A, test fix

Changes
With isr enabled, middlewareMode: 'edge' is silently inert.
The adapter builds and deploys _middleware.func, but every on-demand route's
dest is set to the ISR function, so nothing ever routes to it. Middleware
still runs — but only inside the ISR function, which Vercel skips entirely on a
cache hit. The observable result is middleware that works on a cold entry and
then stops running once the entry is warm, with no error and no log.
This is the ordering problem: middleware has to run before the cache is
consulted, not behind it.
- Route on-demand pages at
_middlewarerather than_isrwhen a middleware
entry point exists, so the edge function is actually reached. - Collect those route patterns and inline them into the generated middleware,
sonext()forwards to/_isr?x_astro_path=…for a route the ISR function
backs, and/_renderfor one it doesn't. Cached responses are still served
from cache; only the entry point moves. _imageand_server-islandskeep going straight to_render, unchanged.- Routes matched by
isr.excludestill resolve to_renderthroughnext(). - Prerendered pages are untouched: no route entry, served as static files.
x_astro_path carries the original pathname, so the ISR cache key stays the
request path rather than collapsing to /_isr; x_astro_path_token is the
build token added in #17370. Without isr, or without a middleware file,
nothing about the output changes.
Testing
New: packages/integrations/vercel/test/isr-edge-middleware.test.ts — 16 tests
over two fixtures.
isr-with-edge-middleware asserts against the real build output, since the bug
is entirely a property of the emitted config.json:
- pages, dynamic routes, endpoints and the 404 resolve to
_middleware _imageand_server-islandsstill resolve to_render- prerendered pages get no route entry and ship as static HTML
- configured
redirectsstill resolve ahead of the middleware - the ISR prerender config and its
expirationsurvive
and then imports the generated middleware.mjs with fetch stubbed, to check
where next() actually forwards:
- a cached route →
/_isr, withx_astro_pathand a token - a dynamic route →
/_isr?x_astro_path=/cached/42, the real path, because that
path is the cache key - an
isr.excluderoute →/_render - a query string does not leak into
x_astro_path - the response still reaches the middleware, headers intact
isr-edge-no-middleware covers middlewareMode: 'edge' with no middleware file
present: routes go straight to _isr as before and no middleware function is
built.
Verified as a regression guard by reverting src/ and re-running: 4 of the 16
fail, all of them on dest.
Also ran the full suite — core unit (3307), core integration (1240), all 18
integration packages including @astrojs/vercel (58), language-tools (98), and
e2e in chrome and firefox. No new failures; the pre-existing ones
(test/fonts.test.ts cancellations, a handful of e2e) don't touch the adapter.
One caveat worth stating plainly: this is verified against build output and the
generated module, not against a live Vercel deployment. Confirmation on a real
project would be welcome.

Changes
- Removed the
.fluefolder from the repository. We don't use it anymore. - Adds
evals.jsonfiles and an harness that we can test when want. The harness isn't wired to our testing infra because it spends tokens.
Testing
Tested locally and all assertions pass
Docs

Closes #17684
Changes
- Prefixes
ASTRO_DEV_SERVER_APP_IDwithvirtual:(changing it fromastro:server-apptovirtual:astro:server-app), matching the convention already used by the siblingvirtual:astro:appmodule in the same file. Vite'sModuleGraph._resolveUrl()skips URL normalization for IDs that start withvirtual:— without this prefix, Vite appended.jsto the stored URL, so full-reload attempts viarunner.import("astro:server-app.js")failed to match Astro'sresolveIdfilter. - Fixes the error
Failed to load url astro:server-app.jsthat appeared when a third-party Vite plugin (e.g.@tailwindcss/vite) triggered an SSR full reload on a non-page file.
Testing
- No automated test added — reproducing this requires a live dev server with
@tailwindcss/vitetriggering an SSR full reload, which is outside the current unit/integration test infrastructure. - Fix was manually verified against the minimal reproduction from the issue; editing
src/content/test.mdnow produces[vite] program reloadwithout theastro:server-app.jserror, and the server continues serving successfully. Confirmed by the issue reporter (@mavam).
Docs
No docs update needed — this is an internal virtual module ID fix with no user-facing API change.

Closes #17679
Changes
- The
glob()andfile()content loaders now respectprerenderConflictBehaviorwhen a duplicate entry ID is detected. Previously, both loaders always emitted a hardcodedlogger.warn()regardless of the config setting. "error"throwsDuplicateContentEntrySlugErrorduring content sync;"ignore"suppresses the warning entirely;"warn"(the default) preserves the existing behavior.
Testing
- Added tests to
file-loader.test.tscovering"error"(throws),"warn"(logs), and"ignore"(silent) modes for duplicate IDs in thefile()loader. - Added tests to
glob-loader.test.tscovering the same three modes for duplicate IDs in theglob()loader.
Docs
No docs update needed — prerenderConflictBehavior is already documented; this extends its existing behavior to a new context without changing its API or semantics.

Changes
- Fixes
server:defercomponents returning 404 responses on Vercel when usingoutput: "static". - Preserves a dedicated server build directory and packages it through the existing Vercel serverless and middleware path only when Astro runs the SSR build for discovered server islands. Fully static sites continue to emit no serverless function, and server build files are not published as static assets.
Closes #17678
Testing
- Adds a static server-islands fixture that verifies
_render.func, its route configuration, and rendering through the packaged function. - Verifies the server entry is excluded from static assets and a fully static fixture does not create a serverless function.
Docs
- No docs update needed because this restores the documented
server:deferbehavior for static output.
Description
- Replace "Markdown plugins" links
We have recently updated the Markdown guide in Astro Docs (withastro/docs#14297) and the section was renamed "Markdown processor plugins". There was two occurrences in Starlight docs.
- Replace an example using Astro DB
Astro DB has been removed in Astro 7 (withastro/docs#13985) and the Astro DB guide now redirects to the integration page with an aside saying "Removed". I think it is better to use an example that is still relevant.

Changes
Remote images whose URL has no file extension fail with 400 Unsupported format: null under the cloudflare-binding image service (the adapter default).
Astro's baseService.validateOptions only sets options.format when it can infer a source format from the URL, and since #16665 deliberately leaves it undefined otherwise so the image service resolves the format from the source bytes instead — which is what the Sharp service does at sharp.ts#L155-L156. With format undefined, no f parameter is emitted onto the /_image URL, and transformStream treated a missing f as a client error.
Extensionless remote images are the common case here — GitHub avatars like https://avatars.githubusercontent.com/u/192622539?s=200&v=4 have no extension to infer from, which is why every theme author avatar on astro.build is currently broken:
$ curl -sD - -o /dev/null 'https://astro.build/_image?href=https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F192622539%3Fs%3D200%26v%3D4&w=300&h=300'
HTTP/1.1 400 Bad Request
Unsupported format: null
# ...the same URL with an explicit format works
$ curl -so /dev/null -w '%{http_code} %{content_type}\n' '...&w=300&h=300&f=webp'
200 image/webptransformStreamnow takes the source's media type and uses it whenfis absent: SVG sources pass through unchanged, everything else is encoded as WebP. This mirrors core'sresolveDefaultOutputFormat, which webp-encodes every non-SVG source.- Requests that explicitly ask for a format the IMAGES binding cannot produce (e.g.
f=tiff) still return400. - This also fixes SVG images, which core requests as
f=svgand which previously failed the same way — the IMAGES binding cannot emit SVG, so those bytes are served as-is.
Fixes withastro/astro.build#2610
Testing
Added four cases to test/binding-image-service.test.ts (build + preview, exercising the IMAGES binding for real) covering remote/local sources with no f, and SVG passthrough. A local HTTP server serves images from extensionless paths to reproduce the GitHub avatar shape. Also added the no-f case to test/dev-image-endpoint.test.ts.
All four new binding tests fail on main with 400 and pass with this change; the seven pre-existing tests in that file are unaffected.
Note
Two things worth flagging for maintainers, both pre-existing and left untouched here:
caches.defaultis persisted undertest/fixtures/binding-image-service/.wrangler/state/v3/cache, so a previously-cached200can mask a genuine failure for any stable/_imageURL. I had to clear that directory to see the new tests fail onmain.createPreviewServerreturns the requested port rather than the bound one (preview.ts#L97), so the whole suite 404s if port 4321 is already taken.
Docs
No docs changes needed. This restores the documented behavior of image.remotePatterns/image.domains for remote images; there's no API or configuration surface change.

Changes
A deploy that changes rendered output without changing content leaves the validator of a cached page untouched: revalidation resolves to 304, and clients keep HTML that references hashed assets from the previous build (/_astro/<hash>.css is gone after the next build). The cause: every validator the Cloudflare provider can send comes from the caller — cache.set(), CacheHint, routeRules — and describes the content, never the build.
With the CF_VERSION_METADATA binding configured, the provider now reads the Worker version id and
- adds an
astro-version:<id>cache tag, which enables version-specific purging, and - folds the id into a weak
ETag(W/"<id>:<lastModified-ms>") on responses that already sendLast-Modifiedand do not supply their ownetag.
Without the binding, every header is exactly what it was before.
Testing
test/cache-provider.test.ts gains four cases against the preview server:
- the version tag appears on a cacheable response,
- the weak
ETagcarries the same id and thelastModifiedtimestamp, - an explicitly supplied
etagsurvives untouched, and - a cacheable response without a validator still gets none.
Two fixture pages (/lastmod, /explicit-etag) and the version_metadata binding in the fixture's wrangler.jsonc support them. The prerenderEnvironment: 'node' build path has no automated case because it would need a second fixture build per run; a manual fixture build with a prerendered route and the provider enabled passes.
Docs
The adapter README does not cover route caching, so there is no section to update here. The behavior is worth a paragraph in the Cloudflare adapter guide on docs.astro.build, and I am happy to open that PR.
Details
Responses that carry no validator keep none. Minting W/"<id>" for them would be the wider fix, but it hands a validator to pages whose content changes between deploys, and a cache may then answer 304 until the next deploy. The narrow rule only makes an existing validator deploy-sensitive, which cannot regress a route.
The version id is read through a dynamic import('cloudflare:workers') resolved once at module load, for the same reason invalidate() imports lazily (#16335): a static import breaks the prerenderEnvironment: 'node' build.
onRequest() would be the other place to reach the runtime, but a provider that defines it counts as a runtime cache, and CacheHandler then strips Cache-Tag from the response.
This picks up #17038, which targeted the feat/cdn-cache-providers branch and was closed when that branch was deleted. The version tag that it built on never reached main, so this change adds it.

Changes
- Moves
satterifromdevDependenciestodependenciesin@astrojs/mdx. The package has unconditional staticfrom 'satteri'imports across four files undersrc/satteri/, making it a runtime requirement. Listing it only as adevDependencymeant pnpm never linked it into@astrojs/mdx's isolatednode_modules, soastro buildfailed withERR_MODULE_NOT_FOUNDin strict pnpm setups (e.g.hoist: false, Vercel monorepo deploys). The import resolved accidentally in non-strict layouts only because pnpm hoistedsatterias a transitive dep ofastro → @astrojs/markdown-satteri.
Testing
- No new tests added. The bug is a packaging/manifest issue that cannot be covered by unit tests run inside the monorepo (which hoists all deps). The fix was confirmed in an isolated
hoist: falsepnpm project by @danielmlr, and by thepackageExtensionscounter-check in their report.
Docs
- No docs update needed; this is an internal dependency declaration fix with no user-visible API change.
Closes #17371

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
astro@7.2.2
Patch Changes
-
#17611
9bc3207Thanks @thelazylamaGit! - Fixes component styles rendered from content entries remaining stale until a second save when an adapter uses Astro's fallback development environment -
#17634
2267eeeThanks @astrobot-houston! - Fixes incremental builds dropping optimized images for cached pages when using acollectStaticImagesprerenderer (e.g.@astrojs/cloudflarewith compile-time image optimization) -
#17650
4cdf128Thanks @astrobot-houston! - Fixes intermittentImageNotFounderrors during build on projects with many images. The build now limits concurrent image file reads to avoid exhausting OS file descriptors (EMFILE) and retries transient I/O errors with backoff. Non-transient errors are no longer silently swallowed. -
#17683
2378221Thanks @astrobot-houston! - FixesprerenderConflictBehaviornot applying to content collection duplicate ID warnings in theglob()andfile()loaders. Setting it to'error'now throws during content sync, and'ignore'suppresses the warning. -
#17659
90c6ea4Thanks @astrobot-houston! - Fixes the Fonts API breakingexperimental.incrementalBuildcaching by embedding a build-local, randomly-assigned server port in generated code used for the dependency hash -
#17630
fd1d9eeThanks @ericclemmons! - Fixes incremental builds becoming prohibitively slow for sites with many pages or content entries that share a large dependency graph. -
#17690
93beeccThanks @NgoQuocViet2001! - Prevents files in directories whose names start withpagesfrom being treated as page routes -
#17671
09f0dc7Thanks @tarikermis! - Fixesastro devrefusing to start after a Docker container restart when an unrelated process reuses the PID from a persisted lock file. Astro now checks the process command across platforms, so stale lock files are cleaned up and--forcedoes not signal the unrelated process.
@astrojs/node@11.1.2
Patch Changes
- #17400
c1cf110Thanks @tianrking! - Return a 404 instead of a 500 for unknown parameters that match a prerendered dynamic endpoint.

Closes #17656
Changes
- Checks the command for the PID stored in the dev or preview lock file instead of assuming that any live process with the same PID is Astro.
- Uses an exact PID lookup on Linux, macOS, and Windows. This cleans up stale locks after container restarts and prevents
--forcefrom signalling an unrelated process when the command can be verified.
Testing
- Added Unix and Windows command matching cases, including the Windows
.cmdshim and unrelated commands. - Added coverage for matching processes, reused PIDs, unavailable process lookups, and stale lock cleanup through the real process lookup.
Docs
No docs update needed because the CLI workflow and user-facing messages stay the same.

Changes
- What does this change?
- Be short and concise. Bullet points can help!
- Before/after screenshots can help as well.
- Don't forget a changeset! Run
pnpm changeset. - See https://contribute.docs.astro.build/docs-for-code-changes/changesets/ for more info on writing changesets.
Testing
Docs

Changes
This PR adds a configuration file that adds some configuration for the upcoming automated reviews.
Testing
Tested in a different repository
Docs
N/A

Changes
- The TS plugin is registered globally, so it runs for every project in a workspace. Since #17269 it calls
addAstroTypes()unconditionally, andfindAstroPackageDirectory()just walks up the tree looking fornode_modules/astro/. With a hoistednode_modules(pnpmnodeLinker: hoisted, npm, classic Yarn) that lookup succeeds from any sibling project, soenv.d.tsandastro-jsx.d.tswere injected into projects that have nothing to do with Astro. Those files transitively pull in@types/node, and its global shims then win overlib.dom.d.ts, so "Go to Definition" onBlob,fetchorURLin a browser-only app lands in@types/node. - Adds an
isAstroProject()guard in front of the injection: the nearestpackage.jsonhas to listastro, or there has to be anastro.config.*next to it. The language server already does this throughgetAstroInstall(), the plugin was the one place missing it.
Closes #17667
Testing
- Three cases in
packages/language-tools/ts-plugin/test/units/astro-types.test.mtsover a fixture monorepo with a hoistednode_modules: a React project that only reachesastrothrough the shared root is skipped, a project that depends onastrois detected, and so is one with anastro.config.mjsbut no dependency.
Docs
- No docs change, this only narrows when the plugin injects its own ambient types.

Changes
Error stack traces printed to the terminal lose every other frame.
formatErrorStackTrace filters the stack with a module-level global regex:
const STACK_LINE_REGEXP = /^\s+at /g;
...
const stackLines = (err.stack || '').split('\n').filter((line) => STACK_LINE_REGEXP.test(line));RegExp.prototype.test on a /g regex advances lastIndex on every match. The next call starts searching mid-string, where ^ cannot match, so it returns false — and then resets lastIndex to 0, which lets the frame after that match again. The result is that exactly half the frames are dropped:
const STACK_LINE_REGEXP = /^\s+at /g;
const stack = [
'Error: boom',
' at first (/app/src/pages/index.astro:3:1)',
' at second (/app/src/lib/a.ts:10:5)',
' at third (/app/src/lib/b.ts:2:2)',
' at fourth (/app/src/lib/c.ts:4:4)',
];
stack.filter((l) => STACK_LINE_REGEXP.test(l));
// → [ 'at first …', 'at third …' ] // second and fourth are gone
stack.filter((l) => /^\s+at /.test(l));
// → [ 'at first …', 'at second …', 'at third …', 'at fourth …' ]IRRELEVANT_STACK_REGEXP on the next line has the same shape — it is also only ever asked whether one line matches, and it feeds findIndex, which decides where the stack gets truncated.
Neither regex needs the g flag, so this drops it from both. Nothing else changes: both are still single-line predicates.
Testing
Added packages/astro/test/units/errors/format-error-message.test.js:
- every frame of a four-frame stack survives
formatErrorMessage() - formatting the same error twice produces the same string (the leaked
lastIndexis per-regex module state, so repeated calls were order-dependent)
The first test fails on main with missing stack frame: at second (/app/src/lib/a.ts:10:5) and passes with this change.
Docs
Not applicable — no API or behaviour that is documented changes; printed stacks just stop losing lines.

Changes
- When
'unsafe-inline'is present inscript-src,style-src,script-src-elem, orstyle-src-elem, Astro no longer emits auto-generated hashes on that directive. Per the CSP spec, browsers silently ignore'unsafe-inline'when a hash is present in the same directive — so users who need'unsafe-inline'(e.g. to allow third-party scripts that inject dynamic inline styles) were getting a broken policy with no workaround. - Adds a
hasUnsafeInline()helper incsp.tschecked at three points: thescript-srcbaseline, thestyle-srcbaseline, and insiderenderSpecificDirective()for-elemvariants. The previous fix (#14798) had only addressedstyle-src-attr, which happened to never emit hashes anyway. - Updates the
security.cspconfig docs to describe this suppression behavior.
Testing
- 7 new unit tests in
packages/astro/test/units/csp/render-csp.test.tscovering: hash suppression onstyle-src,script-src,style-src-elem,script-src-elem, render-time extra hashes withunsafe-inline, and a guard confirming hashes are not suppressed when'unsafe-inline'is scoped only to-attr(which doesn't affect the baseline directive).
Docs
- Inline JSDoc in
packages/astro/src/types/public/config.tsupdated to document the hash-suppression behavior when'unsafe-inline'is used.
Closes #17663

Changes
- The
virtual:astro:assets/fonts/runtime/font-file-url-resolvervirtual module was embedding the font HTTP server'sAddressInfo(including an OS-assigned ephemeral port) as a JSON literal directly in its compiled source text. Because the port changes on every build, any route that transitively imports<Font />got a differentdependencyHashon every run, silently defeatingexperimental.incrementalBuildfor any project using the Fonts API from a shared layout. - Fixes this by assigning the server address to a named variable (
__ASTRO_FONTS_SERVER_ADDRESS__) in the generated module, then stripping that variable declaration inresolveAssetPlaceholders()(inplugin-incremental.ts) before the module code is hashed. The stable variable reference remains in the hashed code; only the volatile declaration (with the changing port) is removed. This follows the same pattern already used to normalize asset emit handles before hashing.
Closes #17626
Testing
- Added
packages/astro/test/incremental-build-fonts.test.ts: runs two consecutive builds of a fixture that uses the Fonts API withexperimental.incrementalBuild: trueand asserts the route'sdependencyHashis identical across both builds. - Added the corresponding fixture (
packages/astro/test/fixtures/incremental-build-fonts/) with a dynamic route using<Font />and a stablecacheKeyfromgetStaticPaths().
Docs
No docs update needed — this is a bug fix for two experimental features; no user-facing API or behavior contract changed.

Changes
- Fixes a
MaxListenersExceededWarningthat fires after ~11 keep-alive requests whenstaticHeaders: trueandsecurity.cspare both active on the Node adapter.serve-static.tscallscreateRequestFromNodeRequest()solely for route matching viaapp.match(), but that function wires anAbortControllercloselistener on the socket that was never cleaned up. On keep-alive connections the listener count grows by one per request. The fix addsgetAbortControllerCleanup(req)?.()immediately afterapp.match(), using the same cleanup pattern already applied toserve-app.tsin #15735.
Testing
- Added a
'Static headers listener cleanup'test suite topackages/integrations/node/test/static-headers.test.tsthat sends 30 keep-alive requests and asserts noMaxListenersExceededWarningis emitted.
Docs
- No docs update needed; this is an internal resource-management fix with no user-facing API change.
Closes #17657

Overview
Adds graceful shutdown support to the Node.js standalone adapter. When the process receives SIGTERM or SIGINT, the server stops accepting new connections and waits for all in-flight requests to complete before exiting.
What's new
- SIGTERM / SIGINT handling — the server closes gracefully on either signal
- Force-destroy timeout — if connections don't drain within the timeout, remaining connections are force-destroyed. Default is 10s, configurable via
ASTRO_NODE_GRACEFUL_SHUTDOWN_TIMEOUT(milliseconds) - Opt-out — set
ASTRO_NODE_GRACEFUL_SHUTDOWN=disabledto skip signal handler registration entirely
Testing
Adds an integration test suite (graceful-shutdown.test.ts) covering:
- No new connections accepted after
server.close() - SIGTERM and SIGINT each independently stop the server (SIGINT tested on a fresh server)
- Multiple concurrent in-flight requests all complete before
closed()resolves - Force-destroy fires after timeout when a connection is permanently stalled
Docs
Docs have not been updated yet.

Changes
Context #17521
- Replaces
semverwith the smaller ESM-nativeverkitpackage for Astro's Node.js version gate, update checks, integration resolution, and the upgrade CLI. - Removes unused
semverdependencies from@astrojs/ts-pluginwhile preserving existing version-handling behavior.
Testing
- No test changes; the migration preserves the behavior covered by the existing Astro CLI and upgrade tests.
Docs
- No docs update needed because this refactor does not change public APIs or user-facing behavior.

Changes
emitImageMetadatanow uses a concurrency-limited file reader (max 200 simultaneousfs.readFilecalls) to prevent exhausting OS file descriptors on projects with tens of thousands of images. Previously, all image imports were read concurrently with no limit, causingEMFILE: too many open fileserrors — especially afterastro checkorastro devalready consumed file descriptors.- Transient I/O errors (
EMFILE,ENFILE,EAGAIN,EBUSY) are retried with exponential backoff instead of failing immediately. - The bare
catchthat silently swallowed all errors (includingEMFILE) is replaced: onlyENOENTreturnsundefined; other errors are re-thrown with the real OS error message. This makesImageNotFoundaccurate — it now only fires when the file genuinely doesn't exist.
Closes #17649
Testing
- Added
packages/astro/test/units/assets/emit-image-metadata.test.tscovering:undefinedid returnsundefined, a missing file (ENOENT) returnsundefined, and a real JPEG file returns correct width/height/format metadata.
Docs
No docs update needed — this is a build reliability fix with no user-facing API change.

Changes
-
The adapter injects a
SESSIONKV binding with noid, which Wrangler treats as a
request to auto-provision a namespace on deploy. That needs an API token with
Workers KV Storage: Edit, so deploys without it fail with a generic
Authentication error [code: 10000]against/storage/kv/namespaces— after the
build already succeeded, and with nothing in the message pointing back at sessions. -
astro buildnow warns when that binding is about to be provisioned, naming the
permission and both escape hatches:[@astrojs/cloudflare] The "SESSION" KV binding has no `id`, so `wrangler deploy` will provision a new KV namespace. This requires an API token with the "Workers KV Storage: Edit" permission. To use an existing namespace, add `kv_namespaces: [{ binding: "SESSION", id: "<id>" }]` to your Wrangler config. To skip sessions entirely, set `session: false` in your Astro config. -
Silent when the user already declared the binding, when
session: false, and during
dev(which never provisions anything). Deduplicated to one warning per build, since
the customizer runs once per worker (entry, prerender, previews). -
No change to the emitted Wrangler config. The existing
!needsSessionKVBinding || hasSessionBindingternary is extracted into a named
injectsSessionBindingso the warning condition and the emission condition can't
drift apart, but the output is identical. -
The warning lives in
cloudflareConfigCustomizerrather than theastro:config:setup
hook because that's the only place that knows whether an id-less binding is actually
being emitted — warning fromindex.tswould also fire for users who correctly
declaredSESSIONwith anid.
Closes #17640
Testing
Five cases added to the existing test/session-false.test.ts, which already covers the
sibling session: false behavior: warns with both key phrases, dedupes across workers,
silent when user-declared, silent when sessions disabled, and doesn't throw without a
logger.
Being upfront about verification: the suite imports from dist/, so it needs a full
monorepo install and build, which I did not run locally. I verified the logic by
compiling wrangler.ts standalone and running the same assertions plus three
regressions asserting binding emission is unchanged (8/8 passing). The committed test
file itself has therefore not been executed, and tsc -b is unconfirmed — I'd
appreciate CI confirming both.
Docs
No docs change needed: this adds no API or config surface, and the new logger option
is internal to the customizer.
That said, the underlying gotcha — that a default-on adapter feature makes deploy
require Workers KV Storage: Edit — isn't currently called out on the Cloudflare
sessions docs page, and arguably should be.
/cc @withastro/maintainers-docs for feedback!

The remote image fetch in redirectValidation.ts passes no signal or timeout to fetchFn. A slow or unresponsive image origin server will stall the fetch indefinitely - blocking SSR responses in production and the dev server during development. Added AbortSignal.timeout(10_000) to the fetch options so the call aborts after 10 seconds rather than hanging forever.


Changes
- Primary purpose of this pull-request is to refactor internal requests to get rid of the
PipelineandAppclasses as used internally. These were essentially "god objects" that held state related to the server-side app. - The problem with these objects were that there was no way to pass them into
FetchStatewhen access from outside of theAppclass. For example in Cloudflare you can create a custom worker which is the entrypoint to the application. - I realized that the
manifestis the one true god-object in SSR, and we could simply derive all state from that. So this new architecture is much more functional. Derived state is created ascreateManifestMemoandcreateAsyncManifestMemowhich are keyed on themanifest. Anything that needs this state can simply import it now. - Everything else in this PR is just conforming to the above.
Appremains as its the external API for adapters, but mostly just defers to the functional approach now.
Fixes #17591
Testing
- Mostly refactored existing tests which relied in the Pipeline.
Docs
- N/A, just a refactor

Changes
- Fixes
experimental.incrementalBuilddropping optimized images for cached pages when an adapter usescollectStaticImages(e.g.@astrojs/cloudflarewith compile-time image optimization). When a cached page and a re-rendered page share the same source image with different transforms, the merge loop ingeneratePageswas replacing the entire entry with.set(path, entry), discarding transforms thatrestoreStaticImages()had already replayed into the global static image list. The fix merges adapter transforms into existing entries instead of overwriting them, matching the deduplication pattern already used byrestoreStaticImages(). Closes #17633.
Testing
- Added
packages/astro/test/units/build/incremental-images.test.tswith two unit tests: one verifying that restored transforms are preserved when adapter images share the same source path, and one verifying that new source paths from adapter images are still added when no restored entry exists.
Docs
- No docs update needed — this is a bug fix for
experimental.incrementalBuild, and no user-facing API or behavior contract changed.

Changes
- A collection schema transform returning a
Temporal.PlainDateor other class instance no longer throwsDataCloneErrorfromgetCollection()/getEntry(). - Gets rid of
structuredCloneusage, so any types supported bydevalue()should work.
Testing
image-references.test.ts: reworked, removed old testsmutable-data-store.test.ts: asserts image prefixes are stripped to plain srcs and their paths recorded asimageImports, and that entries without images record nothing.
Docs
- N/A, bug fix

Changes
Optimizes experimental cache from O(R × G) to O(G × R) using a Merkle-style hashing algorithm to content roots that heavily share dependencies.
R= # of roots. (e.g. 8,000 content pages)G= graph size.
flowchart LR
subgraph Before["Before: O(R × G)"]
P1["Entry 1"] --> G1["Walk graph"]
P2["Entry 2"] --> G2["Walk graph"]
P3["Entry 3"] --> G3["Walk graph"]
end
subgraph After["After: O(G + R)"]
G["Analyze graph once"] --> H["Hash cache"]
H --> R1["Entry 1: O(1) lookup"]
H --> R2["Entry 2: O(1) lookup"]
H --> R3["Entry 3: O(1) lookup"]
end
Testing
The performance degradation was discovered on my fork of cloudflare-docs where I enabled incrementalBuild. ~10m builds now timed out at 30 minutes.
- TODO: Can this be published on pkg.pr.new to validate?
13:01:47.975
> astro build
13:01:47.976
13:01:58.504
18:01:58 [astro-skills] Setting up Agent Skills Discovery routes
13:01:58.504
18:01:58 [astro-skills] Agent Skills Discovery routes configured
13:02:01.502
18:02:01 [build] Waiting for integration "@cloudflare/nimbus-docs", hook "astro:config:setup"...
13:02:14.225
18:02:14 [WARN] [@cloudflare/nimbus-docs] [nimbus-docs] (nimbus/duplicate-slug, warning) 3 routes are served by an explicit src/pages file that shadows a content entry at the same URL:
13:02:14.225
/ai/models ← src/content/docs/ai/models/index.mdx, src/pages/ai/models/index.astro
13:02:14.225
/ruleset-engine/rules-language/fields/reference ← src/content/docs/ruleset-engine/rules-language/fields/reference/index.mdx, src/pages/ruleset-engine/rules-language/fields/reference/index.astro
13:02:14.225
/workers-ai/models ← src/content/docs/workers-ai/models/index.mdx, src/pages/workers-ai/models/index.astro
13:02:14.225
13:02:14.226
Astro serves the page and drops the content route (deterministic). Intended when a content page wraps a custom page component; verify each shadow is intentional.
13:02:18.146
18:02:18 [content] Syncing content
13:02:18.149
18:02:18 [content] Astro config changed
13:02:18.150
18:02:18 [content] Clearing content store
13:02:28.869
18:02:28 [skills-loader] Loaded 11 skill(s) from "skills"
13:02:41.475
18:02:41 [content] Synced content
13:02:41.488
18:02:41 [types] Generated 27.00s
13:02:41.489
18:02:41 [build] output: "static"
13:02:41.490
18:02:41 [build] mode: "static"
13:02:41.490
18:02:41 [build] directory: /opt/buildhome/repo/dist/
13:02:41.490
18:02:41 [build] Collecting build info...
13:02:41.495
18:02:41 [build] ✓ Completed in 43.00s.
13:02:41.498
18:02:41 [build] Building static entrypoints...
13:04:25.008
18:04:25 [astro-icon] Loaded icons from src/icons, ph, simple-icons, vscode-icons
13:31:14.749
Build took too long and was timed outDocs

Changes
- When both
security.cspandexperimental.clientPrerenderare enabled, Astro now injects a single static<script type="speculationrules">at render time using"source": "document"with a CSS selector (a[data-astro-prefetch]orawhenprefetchAllis enabled). This produces a deterministic payload whose hash can be computed at build time and added to the CSPscript-srcdirective. - The client-side prefetch code detects existing document-source speculation rules in the DOM and skips dynamic per-URL injection, which would generate unwhitelistable hashes.
Closes #17599
Testing
- New unit test file
packages/astro/test/units/csp/speculation-rules.test.tscoveringgenerateSpeculationRulesContent()output shape and hash determinism.
Docs
- No docs update needed; this fixes a bug in the interaction between two existing features without changing any user-facing API or configuration.

Changes
- Bumps the
astropeer dependency in@astrojs/cloudflarefrom^7.0.0to^7.2.0. The adapter importsbeginContentEntryCollection,beginImageCollection,endContentEntryCollection, andendImageCollectionfromastro/app, which were added in 7.2.0. The wider range allowed npm to silently resolveastro@7.1.x, causing a crypticMISSING_EXPORTbuild failure with no install-time warning.
Closes #17622
Testing
- No new tests added — the fix is a manifest correction; existing adapter tests cover the affected code paths.
Docs
- No docs update needed; this corrects a peer dependency declaration, not a user-facing API.

Closes #17624
Changes
- Entries whose frontmatter
slugis an unquoted YAML number (e.g.slug: 20260624) now survive repeated syncs. YAML parses an unquoted number as a JSnumber, but theuntouchedEntriesSet holds string keys from the store. TheSet.deletecall used strict equality, sodelete(20260624)was a no-op against"20260624", leaving the entry marked "untouched" and deleted in the cleanup pass on every sync after the first. generateIdDefault()now callsString(data.slug)instead of relying on theas stringtype assertion, which was compile-time-only and did nothing at runtime.- The
generateIdwrapper also coerces the return value withString()so customgenerateIdcallbacks returning non-string values are handled defensively as well.
Testing
- Added a unit test in
packages/astro/test/units/content-layer/glob-loader.test.tsthat callscontentLayer.sync()twice and asserts the numeric-slug entry (id === '20260624') is present after both syncs and that the entry count is stable. - Added a fixture file
src/content/space/numeric-slug.mdwithslug: 20260624to back the new test case.
Docs
No docs update needed — this is a bug fix for an internal ID coercion issue with no API surface change.

Changes
Update example template to refer to Astro 7.0
Testing
No test changed since this is a simple text update in example template.
Docs
No document changed since this is a simple text update in example template.

Changes
- Dynamic redirect routes in
createRedirectsFromAstroRoutesnow honour the user-configured status code. Previously, the dynamic branch hardcoded301(route.type === 'redirect' ? 301 : 200), so a redirect configured as{ destination: '/new', status: 302 }would be written as301in the_redirectsfile. The static branch already calledgetRedirectStatus(route)correctly; this applies the same call to the dynamic branch. - Since
301is cached essentially permanently by browsers, an incorrect status is very difficult to reverse in production.
Closes #17619
Testing
- Two new test cases in
packages/underscore-redirects/test/astro.test.ts: one verifies that a dynamic redirect with{ destination, status: 302 }emits302, the other verifies that a string-form redirect still defaults to301.
Docs
- No docs update needed — this restores already-documented behavior (configured status is respected).

Changes
- Since #15908 the generated TSX exports a component as
BlogPostAstroComponent, so TypeScript never matches<BlogPost />to it and stops offering "Add all missing imports" or "Add import from …". Completions still worked because they strip the suffix afterwards, while code actions need TypeScript to find the export first. patchTSXnow also re-exports the component under its clean name, which keeps the suffixed function name that avoids conflicts with same-name imports inside the file.rewriteAstroImportTextturns the resultingimport { BlogPost } from './BlogPost.astro'back into a default import. Other named imports from.astrofiles, such asProps, are left alone.- Astro components are now offered once in the auto-import completion list instead of twice.
Fixes #17617
Testing
- New
packages/language-tools/language-server/test/typescript/code-actions.test.tsasks the server for quick fixes on an unimported component and checks the import edit. It fails onmain. - New
patchTSXandrewriteAstroImportTextunit tests, plus a count assertion on the existing component auto-import completion test. - The
@astrojs/language-serverand@astrojs/checksuites pass.
Docs
None, this is a bug fix with no API change.

Changes
- closes #17615
- With
experimental.incrementalBuild, a route that imports more than one asset was sometimes re-rendered even though nothing about it changed. An imported image's module code carries an__ASTRO_ASSET_IMAGE__<handle>__placeholder, other assets carry Vite's__VITE_ASSET__<handle>__, and the handle comes fromemitFilein the order the modules finish transforming, so two builds of the same sources can swap the handles around. The route hash then changes while the output stays byte-for-byte identical. - Before hashing a module, the placeholders are replaced with the file name each handle resolves to. Those names are content-hashed, so the hash stays stable across builds and still changes when an asset's contents change. Replacing the placeholder with a fixed token would also stop the churn, but then an image edited without a dimension change would leave the hash untouched and the restored HTML would point at a file name the build no longer emits.
Testing
New unit tests in test/units/build/plugin-incremental.test.ts drive the plugin with swapped emit handles, for images and for other assets, and assert the hash is unchanged, plus the reverse case where a different resolved file name does change it. Also confirmed against the reproduction in the issue, where 50 builds now produce the same hash, and against a variant of it importing a video and a PDF rather than images.
Docs
No user-facing behavior change, so nothing to document.

Fixes: #17621
When navigating away from a route with ClientRouter and then returning to it, later CSS edits can trigger an HMR update without changing the visible page. A manual reload temporarily restores HMR until another navigation causes the problem again.
Vite keeps references to the <style data-vite-dev-id> elements it creates and updates those same elements during CSS HMR. ClientRouter head swaps can remove a Vite-managed style element and replace it with a new element containing the same CSS. Vite still holds the original element reference, so subsequent updates are applied to a detached element instead of the stylesheet currently in the document.
This can affect any route-specific style managed by Vite, including Svelte and other framework component styles, imported CSS, and Astro component styles. Svelte components make the problem particularly visible because their styles can be inserted asynchronously after hydration. Vue already has special handling for browser-transformed scoped styles, which this change preserves.
Changes
- Tracks Vite-managed style elements by their
data-vite-dev-idand reuses the existing element during ClientRouter head swaps. This preserves the element reference used by Vite’s CSS HMR runtime. - Observes styles inserted asynchronously by client framework runtimes so they can also be retained across later navigations.
- Refreshes the contents of server-generated styles when the same Vite ID represents different CSS on the next route, while preserving Vue scoped styles that have been transformed in the browser.
Testing
- Adds coverage for navigating away from and back to a route before updating a nested Svelte component style and an imported stylesheet.
- Verifies that both updates use native CSS HMR without causing a full page reload.
- Adds coverage using a real Vite-created style node to verify that ClientRouter preserves its identity while applying changed CSS from the incoming document.
- Existing Vue scoped-style tests continue to cover preservation of browser-transformed CSS.
Docs
No docs changes. This fixes development-only ClientRouter and Vite HMR coordination without changing public APIs or production output.

Fixes: #17672
When editing a style block in an Astro component rendered from an MDX content entry, the first save triggers a reload but the page can still render the previous CSS. Saving the file again triggers another reload and finally displays the change.
This happens when Astro cannot run the adapter’s SSR environment directly and uses its internal astro environment to collect styles from content entries. In my case this was because I was using the Cloudflare workerd environment.
Astro’s dev CSS plugin does not currently run in the fallback environment, so the first update reloads the page before that environment has collected the latest transformed CSS.
This change enables the existing dev CSS plugin in the fallback astro environment. The updated CSS is then collected during the first file change, allowing the first reload to render the new styles instead of requiring a second save.
Changes
- Applies the dev CSS collection plugin to Astro’s fallback runnable environment, used when an adapter’s SSR environment cannot be run directly, such as with Cloudflare workerd.
- Ensures content-rendered styles are refreshed before an SSR reload, instead of requiring a second save for the latest CSS to appear.
Testing
- Adds coverage verifying that the dev CSS plugin applies to the fallback
astroenvironment.
Docs
No docs changes. This fixes internal development-server CSS collection behavior.

Changes
Add a test case missed in #17605
Testing
This change is only the addition of a test case
Docs
No docs needed for the addition of a test case

PR #17383 fixed stale server-rendered CSS during HMR, but its client-module detection included a broad same-file fallback intended to support speculative query variants such as ?used and ?direct.
Those variants are not part of the verified Astro/Vite update path, and another style module associated with the same source file is not necessarily capable of applying the update. It could represent a different component style index or a non-injected CSS import.
This follow-up removes hasClientStyleModuleByFile() and retains only the module matching behavior supported by observed requests and Astro’s CSS collection code.
Changes
- Removes the same-file fallback previously used to decide whether a client style module could handle an update.
- Uses exact client module IDs when selecting native Vite CSS HMR. The only normalized difference is the bare
inlineparameter that Astro adds when loading component CSS text server-side. - Keeps
.css?rawand.css?inlineimports on the SSR invalidation path because they export strings rather than injecting client styles.
Testing
- Updates coverage to verify that a different style index from the same component does not count as a matching client module.
- Adds coverage for Astro’s server-side
inlinecomponent requests mapping to their exact client equivalent. - Covers CSS string imports, reordered component-style queries, and incomplete or non-style requests.
Docs
No docs changes. This tightens internal HMR module matching without changing public APIs.

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
astro@7.2.1
Patch Changes
-
#17612
7133730Thanks @thelazylamaGit! - Fixes CSS hot module replacement after navigating between pages withClientRouter -
#17628
4ada248Thanks @astrobot-houston! - Fixes a CSP violation when using bothsecurity.cspandexperimental.clientPrerenderwithdata-astro-prefetchlinks. The dynamically injected<script type="speculationrules">now uses a static"source": "document"approach with a CSS selector, producing a deterministic payload that is hashed and included in the CSPscript-srcdirective at build time. -
#17605
89e4647Thanks @ashleigh-yeoman! - Fixes middleware HMR not responding to changes in imported modules. Previously, only direct edits to the middleware file would trigger a reload. -
#17582
bd2c1a5Thanks @astrobot-houston! - Fixes a regression where content collectionreference()fields silently accepted entry IDs that don't exist, such as an ID that doesn't match a loader's slugified version of it. Astro now logs an error for references that point to a missing entry after all loaders finish syncing. -
#17661
97b0cc7Thanks @ArmandPhilippot! - Improves Markdown options documentation with links to the Markdown guide and official processors. -
#17349
4328c73Thanks @astrobot-houston! - Fixes an issue where requests handled by the dev prerender environment (e.g./_imagewith@astrojs/cloudflare'sprerenderEnvironment: 'node') returned a 500 when a prerendered catch-all route existed, because non-prerendered route modules were imported in an environment where their runtime-specific APIs are unavailable -
#17603
722eed6Thanks @astrobot-houston! - Fixes<video>and<audio>elements being non-functional after navigating via view transitions (<ClientRouter />) -
#17616
3a890d2Thanks @lazerg! - Fixesexperimental.incrementalBuildre-rendering unchanged routes that import more than one asset. The route's dependency hash depended on the order the assets finished building, so two builds of identical sources could produce different hashes. The hash is now based on the file name each asset resolves to. -
#17547
fba468cThanks @dmgawel! - ImprovesgetCollection()andgetEntry()performance for entries without local image references -
#17602
16e0d9dThanks @astrobot-houston! - Fixes a build error caused by hash collisions in generated content collection image import identifiers
@astrojs/cloudflare@14.2.1
Patch Changes
-
#17627
ba6a9f6Thanks @astrobot-houston! - Fixes theastropeer dependency range from^7.0.0to^7.2.0. The adapter imports symbols (beginContentEntryCollection,beginImageCollection,endContentEntryCollection,endImageCollection) fromastro/appthat were added in Astro 7.2.0, so earlier versions fail at build time with aMISSING_EXPORTerror. -
Updated dependencies [
0891ac9]:- @astrojs/underscore-redirects@1.0.4
@astrojs/netlify@8.2.1
Patch Changes
- Updated dependencies [
0891ac9]:- @astrojs/underscore-redirects@1.0.4
@astrojs/node@11.1.1
Patch Changes
- #17658
8b211a5Thanks @astrobot-houston! - Fixes an EventEmitter memory leak when serving static pages over keep-alive connections withstaticHeadersenabled and CSP (security.csp) active
@astrojs/language-server@2.16.14
Patch Changes
- #17618
2630631Thanks @lazerg! - Fixes the missing "Add all missing imports" and "Add import from" quick fixes for Astro components
@astrojs/underscore-redirects@1.0.4
Patch Changes
- #17620
0891ac9Thanks @astrobot-houston! - Fixes dynamic redirect routes to honour user-configured status codes instead of hardcoding 301. Previously, a redirect configured with{ destination: '/new', status: 302 }would be emitted as 301 in the_redirectsfile when the route was dynamic.

Changes
Fixes the three @astrojs/cloudflare session-false tests failing on main and blocking #17558.
The adapter's astro:config:setup gained an unguarded read of config.experimental.collectionStorage in #17543 (src/index.ts:289). session-false.test.ts calls that hook with a mock config that has no experimental key, so it threw TypeError: Cannot read properties of undefined (reading 'collectionStorage'). This PR adds the missing key to the mock:
experimental: { collectionStorage: 'single-file' },A resolved AstroConfig always has it, since the schema defaults it to 'single-file', so the mock was incomplete rather than the adapter.
Testing
- Fixed the failing Cloudflare tests.
- Node and Netlify are unaffected since their mocks don't read
experimental.
Docs
None needed, test-only change. No changeset for the same reason.

Changes
Fixes the previous attempt to get HMR working for middleware. Now any imports (and transitive imports) in middleware.ts can trigger HMR for middleware.ts, instead of just itself.
This is meant to be a cleaner and better-tested version of #17597
Closes #17590
Testing
3 tests added to packages/astro/test/middleware.test.ts, covering the 3 main HMR cases
middleware.tsis modified- A module imported by
middleware.tsis modified - A transitive dependency (a module imported by a module imported by
middleware.ts) is modified
Docs
No docs update needed. This PR fixes a bug in HMR behavior, with no API changes.

Changes
<video>and<audio>elements are now fully functional after navigating to a page via<ClientRouter />. Previously, media controls were permanently disabled and playback was impossible after any client-side navigation.- The root cause is
DOMParser.parseFromString(), which parses incoming page HTML into an inert document where the browser never initializes the media stack. Moving those elements into the live DOM does not retroactively initialize it. The fix adds areifyMediaElements()post-swap step (following the existingattachShadowRoots()pattern) that replaces each<video>/<audio>element with a fresh copy created viadocument.createElement(), forcing the browser to properly initialize the media stack.
Testing
- No automated test added — the fix is client-side browser code that exercises the browser's media stack initialization, which cannot be meaningfully tested in Node.js.
Docs
- No docs update needed; this is a bug fix restoring behavior that was always expected to work.
Closes #17601

Changes
- Content collection image imports in
.astro/content-assets.mjsnow use sequential identifiers (__ASTRO_IMAGE_IMPORT_0,__ASTRO_IMAGE_IMPORT_1, …) instead of hash-based names. The previousshorthash()-derived names used a 32-bit hash that could collide for different image paths, causing aPARSE_ERRORat build time. Sequential indices are collision-free by construction. - Removes the now-unused
importIdToSymbolNameexport andshorthashimport fromresolveImports.ts.
Closes #17595
Testing
- Adds
packages/astro/test/units/content-layer/asset-imports.test.ts: verifies that two image paths whose filenames produce identicalshorthash()values (imgAa.jpg/imgBB.jpg) are assigned distinct import identifiers.
Docs
- No docs update needed — this is an internal codegen detail with no user-facing API change.

Changes
- Fixes #17589
getCollection()/getEntry()threwDataCloneErrorwhen a collection schema produced values thatstructuredClonecannot handle — e.g. a Zodtransform()returningTemporal.PlainDateor any other class instance.- The unconditional
structuredClone(data)inupdateImageReferencesInData()(introduced in #16701 to preserveMap/Set) is replaced with a selective clone: only plain objects and arrays — the containers whose nested strings may be rewritten into image metadata — are copied. Everything else (Map,Set, class instances,Temporalobjects, …) passes through by reference. - This is safe because the traversal only ever calls
ctx.update()on strings prefixed with__ASTRO_IMAGE_; non-plain values are never mutation targets, so sharing them between the store data and the returned copy cannot leak mutations. - Handles circular references among plain containers via a
WeakMap, matchingstructuredClone's behavior there. - Includes a changeset (patch for
astro).
Testing
- Added a regression test in
packages/astro/test/units/content-collections/image-references.test.tsusing a class instance as a stand-in forTemporal.PlainDate(the test first assertsstructuredClonerejects it withDataCloneError, then thatupdateImageReferencesInDatapreserves it by reference, including when nested). - Added a test asserting image resolution still does not mutate the original store data.
- All 13 tests in
image-references.test.tspass, including theMap/Setpreservation tests from #16701; the other content-collections unit suites (get-entry-info,get-entry-type,mutable-data-store) also pass.
Docs
No docs changes needed — this removes an undocumented limitation rather than changing documented behavior.
🤖 Generated with Claude Code
Adds https://lilypond.ky.fyi to the Starlight Showcase following the contribution guidelines.

Changes
app.use(cf())from@astrojs/cloudflare/hononow type-checks correctly in projects that usewrangler types. Previously,HonoCloudflareContextLikedeclaredexecutionCtx: ExecutionContext, binding it to the globalExecutionContexttype. Whenwrangler typesgeneratesworker-configuration.d.ts, that global gains required members (tracing,exports) that Hono's ownExecutionContextdoesn't have, making the handler contravariant-incompatible withMiddlewareHandler.- Replaces
executionCtx: ExecutionContextwith an inline structural type listing only the three members actually consumed downstream (waitUntil,passThroughOnException,props). This matches the approachastro/honoalready uses for its duck-typed context, and the narrowed type remains assignable to internal callers (cfFetch,createLocals) without any casts.
Testing
- No new automated test — the mismatch only surfaces when the user's
wrangler types-generatedExecutionContextglobal is in scope, which the package's own test compilation doesn't exercise (it compiles against@cloudflare/workers-types, whoseExecutionContexthas fewer/optional members). The fix was verified against the reporter's repro at https://github.com/iseraph-dev/repro-astro-cf-hono-types, whereastro checkgoes from 1 error to 0.
Docs
- No docs change needed — this is a type-only fix with no behavior or API surface change.
Closes #17593
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@astrojs/starlight@0.41.7
Patch Changes
Description
- Fixes our Sätteri plugin for supporting code in RTL documents in newer versions of Astro
- Fixes the failing test in #4113
- Sätteri now has a
ctx.parent()we can use to get parents so we can remove the more convoluted code for working this out. Strictly speaking these do not work 1:1 — before, we’d skip applying attributes to a<code>element even if it was deeply nested in a<pre>whereas now we only skip if the<code>is a direct child of the<pre>. However, in practice, I think this covers all the scenarios we were intending? - Existing tests here should pass, showing that the changes work for people on older versions of Astro. And I ran the changes against #4113 too to ensure it fixes the failing test there.
Description
- Updates monorepo versions of
astroto latest, fixes #3991 in our own docs - Also updates other
@astrojs/*packages - Updates
sharpto latest in examples and docs
Description
While reviewing another PR, I noticed we use pnpm dlx to run pkg-pr-new for preview releases.
This is not recommended because pnpm dlx would always resolves to the latest version which could have unexpected behavior due to some upstream breaking changes but also increases supply-chain risks.
Caution
In CI environments, avoid npx, pnpm dlx, yarn dlx, and bunx for this step. Install pkg-pr-new as a dependency and execute it from the lockfile (npm exec, pnpm exec, yarn, or bun run).
This PR fixes that by installing pkg-pr-new as a dev dependency and running it using pnpm exec.

Changes
I hit this while following up on #17481. The same header mutation that crashed the Cloudflare adapter on cached image responses also lives in core: applyCacheHeaders() calls response.headers.set() directly on the response a route returns, and CacheHandler deletes CDN-Cache-Control and Cache-Tag from the response a provider returns. Both throw TypeError: immutable when the response headers can't be modified, which is the case for any response returned straight from fetch(). A route that proxies an upstream (return fetch(...) plus cache.set({ maxAge })) gets a 500 on every request. A user already reported this error class when combining cache.set() with a Vary header on Cloudflare (comment on #17408). I haven't verified that path, it needs the Cloudflare dev runtime, but the repro here is adapter free and fails on plain Node.
The fix mirrors what #17481 did in the adapter: try the mutation, and when it throws, rebuild the response with new Response(response.body, response) and apply the headers to the copy. The first .set() throws before changing anything, so applying them again can't duplicate headers. Header stripping now also checks has() first, so provider responses without CDN headers aren't rebuilt for nothing.
One heads up: pnpm install with the pinned pnpm also dropped a stale triage/gh-17583 importer from the lockfile that #17584 left behind. I can split that out if you'd rather keep it.
Testing
Two new fixtures. Without the fix all four new tests fail with a 500 instead of a 200: a memoryCache() route returning a fetch() proxied response, and a provider whose onRequest serves responses with immutable headers, where the CDN header stripping used to crash even when those headers weren't present. With the fix they pass and the second request is served as a cache HIT. The cache unit suite (162 tests) and the neighbouring integration tests still pass. I also reproduced the crash end to end against published astro 7.1.6 with @astrojs/node before writing the fix.
Docs
No docs changes. The changeset describes the fix.

Changes
Skip the failing type check tests in ecosystem-ci. This will make ecosystem-ci pass.
I think it is better to make the tests pass so we can easily catch regressions. If we want to catch regressions for types, we can enable these tests later when it passes.
I didn't add a changeset as this is completely an internal change.
Testing
I ran ecosystem-ci against my fork by running node ecosystem-ci.ts astro in the local modified ecosystem-ci repo.
Docs
No docs change as this is an internal change.

Changes
Astro's config schema imported @astrojs/markdown-satteri at the top level, so Sätteri and its optional platform-specific native binaries landed in the module graph on every astro dev/build, even for projects with no Markdown files. When npm skips the unavailable optional binary for the host platform (for example Windows without @bruits/satteri-wasm32-wasi), the bundler then can't resolve it and the build crashes.
This puts the default processor behind a dynamic import, so @astrojs/markdown-satteri is only resolved when Markdown actually gets rendered. A project with no Markdown never pulls it into the graph.
Testing
I added a unit test checking that the default markdown.processor still resolves to the Sätteri processor. The existing markdown.processor integration test already covers rendering with the default, and it plus the full unit suite pass. The Windows build crash from the report needs the missing optional binary, so it can't be reproduced on Linux CI.
Docs
No user-facing behavior change, so no docs needed.
Fixes #17585

Changes
- Solid component libraries that ship pre-compiled browser artifacts via the
exports.solidcondition (e.g.@kobalte/core) were left external during prerendering. Node resolved them via thedefaultcondition instead, which picks up browser-only code that callstemplate()and other APIs stubbed withnotSup()insolid-js/web/dist/server.js— crashingastro buildwith "Client-only API called on the server side". - Uses
crawlFrameworkPkgsfromvitefuto discover all packages that declaresolid-jsas a peer dependency, then adds them toresolve.noExternalfor non-client environments (e.g.prerender). This forces Vite to bundle those packages so it can apply thesolidexport condition correctly, matching the established pattern from@astrojs/svelte(PR #16210). - Adds
vitefuto@astrojs/solid-js'sdependencies(previously it was only available as a transitive dep via other packages).
Testing
No automated test added — configEnvironmentPlugin is internal and the solid integration has no existing test infrastructure for this; the svelte integration's equivalent fix also lacks a unit test. Fix was confirmed working by the reporter against both a minimal reproduction and a production project (12 pages).
Docs
No docs update needed — this restores expected build behavior with no API changes.
Closes #17583

Changes
reference()fields in content collections no longer silently accept entry IDs that don't exist in the store. After all loaders finish syncing,ContentLayernow walks every entry's data to find reference objects ({ id, collection }) and logs an error for any that point to a missing entry. This catches cases likeauthor: "John-Doe"where theglob()loader slugified the actual entry ID tojohn-doe.- This is a regression from the Zod 4 upgrade (PR #14956), which removed the inline Zod validation that previously caught invalid references. Inline validation can't be restored because loaders run in parallel and the referenced collection may not be populated yet, so validation runs post-sync instead.
Closes #17322
Testing
- Two new unit tests in
packages/astro/test/units/content-layer/data-transforms.test.ts: one verifying that an invalid reference (John-Doewhere onlyjohn-doeexists) produces an error log, and one verifying that a valid reference produces no error.
Docs
- No docs update needed; this restores previously documented behavior that was accidentally removed.

Changes
package-manager-detector recently supported the devEngines field, but for our custom strategies option passing, we redefine the list and didn't include the devEngines-field option, so this PR includes it.
Its default value had devEngines-field.
Testing
Didn't test as should be a simple change
Docs
Added a changeset.

This PR contains the following updates:
Release Notes
mochajs/mocha (mocha)
v11.8.0
v11.7.6
🩹 Fixes
- make
describe().timeout()work (aafe6fd) - test: replace
wmicusage with native Windows API (#5694) (73ebdfa)
🧹 Chores
prettier/prettier (prettier)
v3.9.6
v3.9.5
Markdown: Cap ordered list mark at 999,999,999 (#19351 by @tats-u)
CommonMark parsers only support ordered list item numbers up to 999,999,999.
With this change, Prettier now caps the ordered list item number at 999,999,999 to ensure that the output is correctly parsed as an ordered list by CommonMark parsers. Numbers larger than 999,999,999 are not parsed as list item numbers and are left unchanged in the output:
<!-- Input -->
999999998. text
999999998. text
999999998. text
999999998. text
1234567890123456789012) text
<!-- Prettier 3.9.4 -->
999999998. text
999999999. text
1000000000. text
1000000001. text
1234567890123456789012) text
<!-- Prettier 3.9.5 -->
999999998. text
999999999. text
999999999. text
999999999. text
1234567890123456789012) textMarkdown: Avoid corrupting empty link with title (#19487 by @andersk)
Do not remove <> from an inline link or image with an empty URL and a title, as this removal would change its interpretation.
<!-- Input -->
[link](<> "title")
<!-- Prettier 3.9.4 -->
[link]( "title")
<!-- Prettier 3.9.5 -->
[link](<> "title")Less: Remove extra spaces after [ in map lookups (#19503 by @kovsu)
// Input
.foo {
color: #theme[ primary];
color: #theme[@name];
color: #theme[@@name];
}
// Prettier 3.9.4
.foo {
color: #theme[ primary];
color: #theme[ @name];
color: #theme[ @@name];
}
// Prettier 3.9.5
.foo {
color: #theme[primary];
color: #theme[@name];
color: #theme[@@name];
}CSS: Prevent addition space in type() with + (#19516 by @bigandy)
This fixes the addition space before + in CSS type() declaration. For example type(<number>+) was being converted into type(<number> +) which is invalid CSS and does not work.
/* Input */
div {
border-radius: attr(br type(<length>+));
}
/* Prettier 3.9.4 */
div {
border-radius: attr(br type(<length> +));
}
/* Prettier 3.9.5 */
div {
border-radius: attr(br type(<length>+));
}Less: Remove spaces between merge markers and colons (#19517 by @kovsu)
// Input
a {
box-shadow + : 0 0 1px #​000;
}
// Prettier 3.9.4
a {
box-shadow+ : 0 0 1px #​000;
}
// Prettier 3.9.5
a {
box-shadow+: 0 0 1px #​000;
}Markdown: Preserve wiki links with aliases (#19527 by @kovsu)
<!-- Input -->
[[Foo:Bar]]
<!-- Prettier 3.9.4 -->
[[Foo]]
<!-- Prettier 3.9.5 -->
[[Foo:Bar]]TypeScript: Fix comments being dropped on shorthand type import/export specifiers (#19565 by @kirkwaiblinger)
// Input
export { type /* comment */ T } from "foo";
import { type /* comment */ T } from "foo";
// Prettier 3.9.4
Error: Comment "comment" was not printed. Please report this error!
// Prettier 3.9.5
export { type /* comment */ T } from "foo";
import { type /* comment */ T } from "foo";Miscellaneous: Preserving comments' placement property (#19567 by @Janther)
Prettier@3.9.0 deleted an undocumented property on comments, which was already used by plugins, comment.placement is now available again after comment attach.
Flow: Stop enforcing empty module declaration to break (#19568 by @fisker)
// Input
declare module "foo" {}
// Prettier 3.9.4
declare module "foo" {
}
// Prettier 3.9.5
declare module "foo" {}
Angular: Support expression for exhaustive typechecking (#19571 by @fisker)
<!-- Input -->
@switch (state.mode) {
@default never(state);
}
<!-- Prettier 3.9.4 -->
@switch (state.mode) {
@default never;
}
<!-- Prettier 3.9.5 -->
@switch (state.mode) {
@default never(state);
}TypeScript: Ignore comments inside mapped type when checking type parameter comments (#19572 by @fisker)
// Input
foo<{
// comment
[key in keyof Foo]: number
}>();
// Prettier 3.9.4
foo<
{
// comment
[key in keyof Foo]: number;
}
>();
// Prettier 3.9.5
foo<{
// comment
[key in keyof Foo]: number;
}>();Less: Fix adjacent block comments being corrupted (#19574 by @kovsu)
// Input
/* a *//* b */
/* a */* {
color: red;
}
// Prettier 3.9.4
/* a */
/* b */
/* a * {
color: red;
}
// Prettier 3.9.5
/* a */ /* b */
/* a */
* {
color: red;
}JavaScript: Handle dangling comments in SwitchStatement (#19581 by @fisker)
// Input
switch (foo) {
// comment
}
// Prettier 3.9.4
switch (
foo
// comment
) {
}
// Prettier 3.9.5
switch (foo) {
// comment
}TypeScript: Remove space in comment-only object type (#19583 by @fisker)
// Input
var foo = {
/* comment */
};
type Foo = {
/* comment */
};
// Prettier 3.9.4
var foo = {/* comment */};
type Foo = { /* comment */ };
// Prettier 3.9.5
var foo = {/* comment */};
type Foo = {/* comment */};v3.9.4
v3.9.3
v3.9.2
v3.9.1
sveltejs/svelte (svelte)
v5.56.8
Patch Changes
-
fix: call
onerrorand provide a workingresetwhen hydrating a failed boundary (#18556) -
fix: preserve select selection when spread attributes omit value (#18561)
v5.56.7
Patch Changes
- chore: provide
indentoption forprint(#18474)
v5.56.6
Patch Changes
-
perf: skip unnecessary blocker analysis when compiling components without top-level await (#18548)
-
fix: rerun derived that had an abort controller on reconnection (#18551)
v5.56.5
Patch Changes
-
chore: drop dead code that make TSGO fail (#18496)
-
fix: don't (re)connect deriveds when read inside branch/root effects (#18527)
-
fix: skip unnecessary derived effect in earlier batch (#18525)
-
fix: avoid declaration tag warning in event handlers (#18500)
-
fix: abort deriveds own AbortSignal when it disconnects (#18400)
-
fix: ensure
$state.eager()is correctly transormed for SSR output (#18530) -
fix: correctly transform declaration tags during SSR (#18492)
-
fix: transform computed keys in keyed
{#each}destructuring patterns (#18521) -
fix: chain preprocessor sourcemaps with an empty
sources[0]instead of dropping them (#18518) -
fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens (#18541)
-
fix: don't treat declaration tags as parts inside each blocks (#18507)
v5.56.4
Patch Changes
-
fix: include wrapping parentheses in
{@const}declaratorendposition (#18436) -
fix: always unset reactivity context after restoring it (#18453)
-
fix: don't notify
searchParamssubscribers when the URL changes without affecting the search string (#18425) -
fix: strip
?from optional parameters in<script lang="ts">so generated JavaScript is valid (#18448)
v5.56.3
Patch Changes
-
fix: ignore errors that occur in destroyed effects (#18384)
-
fix: type BigInts in
$state.snapshot(...)return values (#18388)
v5.56.2
Patch Changes
-
fix: properly track effect end node for async sibling component (#18371)
-
fix: prevent false-positive reactivity loss warning (#18373)
-
chore: bump esrap dependency (#18372)
-
fix: ignore declaration tags for animation directive (#18366)
-
fix: reject pending async deriveds on discard (#18308)
v5.56.1
Patch Changes
-
fix: error at compile time on duplicate snippet/declaration tag definitions (#18351)
-
fix: parse declaration tag contents more robustly (#18353)
-
fix: correctly transform references to earlier declarators in a declaration tag (e.g.
{let a = $state(0), b = $derived(a * 2)}) (#18348) -
fix: avoid spurious
state_referenced_locallywarnings for$deriveddeclarations in declaration tags (#18348) -
fix: tolerate whitespace before
let/constin declaration tags (#18348) -
fix: prevent infinite loop when a tag's expression ends with a trailing
/at the end of the input (#18350) -
fix: more robust parsing of declaration tags with regards to
type(#18330) -
fix: preserve newlines in spread input values when the
typeattribute is applied aftervalue(#18345) -
fix: update
SvelteURLSearchParamswhen setting duplicate keys to the same joined value (#18336) -
fix: check references for blockers on server, too (#18352)
v5.56.0
Minor Changes
- feat: allow declarations in the template (#18282)
Patch Changes
-
perf: use
createElementinstead ofcreateElementNSfor HTML elements (#18262) -
perf: store
current_sourcesas aSetfor O(1) membership checks (#18278) -
perf: deduplicate identical hoisted templates within a component (#18320)
-
perf: hoist
rest_propsexclude list as a module-scopeSet(#18252)
v5.55.10
Patch Changes
-
fix: unlink errored and otherwise finished batch (#18264)
-
perf: walk composedPath() directly in delegated event propagation (#18268)
-
fix: transfer effects when merging batches (#18254)
-
fix: allow
$derived(await ...)in disconnected effect roots (#18273) -
fix: remove temporary raw-text hydration markers (#18269)
-
fix: propagate async
@constblockers through closure references so template expressions like{(() => host)()}correctly wait for the awaited value (#18309) -
fix: properly unlink batches (#18298)
-
fix: settle discarded batch (#18290)
-
fix: declare
let:directives before{@const}declarations on slotted elements (#18271) -
fix: resume outro-ed branches if they were kept around (#18291)
-
fix: avoid waterfall-warning when async resolves to same value (#18297)
-
fix: correctly coordinate component-level effects inside async blocks (#18260)
-
fix: make unnecessary commit work less likely (#18263)
-
chore: add tag name to
a11y_click_events_have_key_eventswarning (#18272) -
fix: catch rejected promises while merging/committing (#18266)
v5.55.9
Patch Changes
-
fix: don't unset batch when calling
{#await ...}promise (#18243) -
fix: promise-ify
{#await await ...}expressions on the server and correctly hydrate them on the client (#18243) -
fix: deduplicate dependencies that are added outside the init/update cycle (#18243)
-
fix: avoid false-positive batch invariant error (#18246)
-
fix: inline primitive constants in attribute values during SSR (#18232)
v5.55.8
Patch Changes
-
fix(print): handle
svelte:bodyand fix keyframe percentage double-printing (#18234) -
fix: execute uninitialized derived even if it's destroyed (#18228)
-
fix: use named symbols everywhere (#18238)
-
fix: don't run teardown effects when deriveds are unfreezed (#18227)
-
fix: unset context synchronously in
run(#18236)
v5.55.7
Patch Changes
-
fix: prevent XSS on
hydratablefrom user contents (a16ebc67bbcf8f708360195687e1b2719463e1a4) -
chore: bump devalue (#18219)
-
fix: disallow empty attribute names during SSR (
547853e2406a2147ad7fb5ffeba95b01bd9642da) -
fix: harden regex (
d2375e2ebcab5c88feb5652f1a9d621b8f06b259) -
fix: move Svelte runtime properties to symbols (
e1cbbd96441e82c9eb8a23a2903c0d06d3cda991)
v5.55.6
Patch Changes
-
fix: leave stale promises to wait for a later resolution, instead of rejecting (#18180)
-
fix: keep dependencies of
$state.eager/pending(#18218) -
fix: reapply context after transforming error during SSR (#18099)
-
fix: don't rebase just-created batches (#18117)
-
chore: allow
nullforpendingin typings (#18201) -
fix: flush eager effects in production (#18107)
-
fix: rethrow error of failed iterable after calling
return()(#18169) -
fix: account for proxified instance when updating
bind:this(#18147) -
fix: ensure scheduled batch is flushed if not obsolete (#18131)
-
fix: resolve stale deriveds with latest value (#18167)
-
chore: remove unnecessary
increment_pendingcalls (#18183) -
fix: correctly compile component member expressions for SSR (#18192)
-
fix: reset
source.updatedstack traces afterflush(#18196) -
fix: replacing async 'blocking' strategy with 'merging' (#18205)
-
fix: allow
@debugtags to reference awaited variables (#18138) -
fix: re-run fallback props if dependencies update (#18146)
-
fix: abort running obsolete async branches (#18118)
-
fix: ignore comments when reading CSS values (#18153)
-
fix: wrap
Promise.allinsaveduring SSR (#18178) -
fix: ignore false-positive errors of
$inspectdependencies (#18106)
v5.55.5
Patch Changes
-
fix: don't mark deriveds while an effect is updating (#18124)
-
fix: do not dispatch introstart event with animation of animate directive (#18122)
v5.55.4
Patch Changes
privatenumber/tsx (tsx)
v4.23.5
v4.23.4
Bug Fixes
This release is also available on:
v4.23.3
Bug Fixes
This release is also available on:
v4.23.2
v4.23.1
Bug Fixes
- support tsImport after global preload (8d4ffc2)
- watch: avoid clearing piped output (95d0672)
- watch: treat script and dependency paths literally (79fddde)
Performance Improvements
- index transform cache lazily (e818ad6)
- load esbuild lazily in CLI (d067938)
- map Node TypeScript formats directly (cdcc623)
- use sync module hooks on Node v22.22.3+ (f8992f1)
This release is also available on:
v4.23.0
Bug Fixes
- avoid redundant filesystem probes during module resolution (257bbbb), closes privatenumber/tsx#809
Features
- add multi-scenario startup benchmark suite (c178197), closes privatenumber/tsx#809 #809 hi#signal privatenumber/tsx#145 #809
This release is also available on:
v4.22.5
Bug Fixes
- isolate hook state per async module.register() registration (a305f36)
This release is also available on:
v4.22.4
Bug Fixes
This release is also available on:
Configuration
📅 Schedule: (UTC)
- Branch creation
- Between 12:00 AM and 03:59 AM, only on Monday (
* 0-3 * * 1)
- Between 12:00 AM and 03:59 AM, only on Monday (
- Automerge
- At any time (no schedule defined)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
👻 Immortal: This PR will be recreated if closed unmerged. Get config help if that's undesired.
- If you want to rebase/retry this PR, check this box
This PR was generated by Mend Renovate. View the repository job log.

Changes
- closes #17574
imageService: 'custom'(and the fallback case) left Astro's default dev image endpoint in place, which importsviteandnode:fsand cannot load inside workerd, so every/_imagerequest returned 500 in dev- use the generic fetch-based endpoint in dev, matching the other image service modes; a user-configured
image.endpointis left untouched - warn in dev when
imageService: 'custom'resolves to the Sharp service (including when noimage.serviceis configured), since Sharp's native binding cannot run inside workerd in dev or production (see #17574 (comment))
Testing
- new
custom-image-service.test.ts - new
setImageConfigunit tests inimage-config.test.ts - existing
Docs
- none, bug fix

Closes #17508
Changes
- Adds
neotraverseto theALWAYS_NOEXTERNALlist invite-plugin-environment/index.ts, forcing it to be bundled into the prerender output instead of emitted as a bare external import. - Before this fix, if any other package in the dependency tree required
neotraverse@^0.6.x, npm would hoist that older copy to the project root. The prerender bundle (written todist/.prerender/, outsidenode_modules/astro) would then resolve the bareimport { forEach } from "neotraverse"to the hoisted 0.6.x copy — which doesn't exportforEach— crashing the build. Bundlingneotraverseensures Astro's own copy is always used regardless of what's hoisted in the project.
Testing
- No new tests added; the fix is a one-line config addition. Existing Content Layer integration tests cover the affected code paths and confirm no regression.
Docs
- No docs update needed; this is an internal dependency resolution fix with no user-facing API changes.

Changes
- Fixes a crash in the Node adapter when a request arrives with a malformed port in the
Hostheader (e.g.example.com:65536,example.com:8080:8080). The bad host made the request URL invalid, and the existingcatchfallback rebuilt the URL from the same host and threw again. buildRequestUrl(shared bycreateRequestFromNodeRequestandcreateRequest) now degrades in steps — full URL, origin only, then a server-controlled host (localhost, with the listening port when known) — so URL construction never throws.parseHostnow rejects a host with more than onehostname:portpair, which previously passed by inspecting only the first two colon-separated segments.
Testing
- Unit tests in
node.test.tscovering malformed hosts throughcreateRequestFromNodeRequest(no throw, parseable URL) plus a valid max-port control, and acreateRequestcase asserting a duplicated-port host is rejected. - An end-to-end
@astrojs/nodetest that drives a standalone server, sends a craftedHostover a raw socket, and asserts a follow-up request still succeeds.
Docs
- No docs update needed; this is an internal reliability fix with no API change.

Closes #17329
Changes
- Cookies set via
Astro.cookies.set()inside a custom404.astroor500.astroare now correctly included in the final response. Previously they were silently dropped. - Two bugs in
mergeResponses(packages/astro/src/core/errors/default-handler.ts): (1) when both the original and error page hadAstroCookies, the error page's cookies were appended tooriginalResponse.headers— an object already copied intonewHeadersand no longer connected to the merged response — so they went nowhere; fixed by replacing the loop withoriginalCookies.merge(newCookies). (2) theseen-set guard that deduplicates merged headers treatedset-cookieas a single-value header, blocking error-page cookies when the original response already set one; fixed by always appendingset-cookieentries.
Testing
- Added
packages/astro/test/error-page-cookies.test.tswith three cases: error page cookies survive when the original page throws, error page cookies survive when no original cookies exist, and both middleware and 404 error page cookies are preserved together.
Docs
- No docs update needed. This restores the behavior that
Astro.cookies.set()is documented to provide; no API surface changed.

Changes
@astrojs/vercelcreates.vercel/output/server/with a plainmkdirSync, soastro buildcrashes withEEXISTwhen that directory already exists, for example when two builds run against the same project root. This creates it with{ recursive: true }, the same way thestatic/directory one line above is already created.- It also awaits
emptyDir(staticDir), which was fire-and-forget before and could race the copy calls that follow it.
Fixes #17568
Testing
The existing @astrojs/vercel test suite passes. Reproducing the race needs two overlapping builds sharing one project root, which the current integration harness has no way to set up, so I did not add a new test.
Docs
No docs changes. This is an internal bug fix with no change to the public API.

Changes
- Fixes
fontProviders.googleicons()downloading the entire ~3.9MB Google Icons font instead of only the requested glyphs whenexperimental.glyphscontains more than one name. The root cause is a bug in unifont (unjs/unifont#336) where glyph names are joined with.join("")(no separator), producing an invalidicon_namesquery parameter that causes Google's API to silently return the full font. The workaround pre-joins multiple glyphs into a single comma-separated string before passing them to unifont, so unifont's.join("")produces the correct value.
Testing
- Adds
packages/astro/test/units/assets/fonts/googleicons-glyphs.test.tscovering theresolveFontcall with multiple glyphs, a single glyph, undefined options, and an empty glyphs array — verifying the patching logic doesn't throw for any of these shapes.
Docs
- No docs update needed;
experimental.glyphsbehavior is unchanged from the user's perspective — this restores the documented subsetting behavior.
Closes #17565
Last fetched: | Scheduled refresh: Every Saturday
See Customizing GitHub Activity Pages to configure your own
Inspired by prs.atinux.com