Astro Info
❯ astro info
Astro v4.5.16
Node v22.18.0
System macOS (arm64)
Package Manager unknown
Output static
Adapter @astrojs/vercel
Integrations @astrojs/vue
If this issue only occurs in one browser, which browser is a problem?
All
Describe the Bug
In Astro 6, when output: 'static' is combined with at least one page that uses export const prerender = false, Astro internally flips the build into hybrid mode. In that mode, the multiple Vite passes (Server, Prerender, Client) compute their own content-addressed hashes, and reconciliation between those passes is incomplete: dynamic-import() URLs that get baked into chunk bodies can reference hashes that were never emitted by the Client pass.
The result is that the prerendered HTML loads fine — the top-level chunk references it imports were reconciled — but a runtime import() call inside one of those chunks resolves to a /_astro/<name>.<wrongHash>.js that doesn't exist on disk. The request 404s, falls through to /404.html (per the Vercel adapter's standard routes), and the browser refuses the module with a MIME error because the response is text/html.
This was not present in Astro 5 / @astrojs/vercel v9 because the /static and /serverless subpath adapters kept the build environments fully separate.
Environment
- astro: 6.1.10
- @astrojs/vercel: 10.0.6
- @astrojs/vue: 6.0.1
- Node: 22.x
- Deployment target: Vercel (Build Output API v3)
Configuration
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'static',
integrations: [vue()],
adapter: vercel(),
trailingSlash: 'never',
vite: {
resolve: { alias: { '@assets': '/src/assets' } },
css: {
preprocessorOptions: {
scss: { api: 'modern-compiler', silenceDeprecations: ['import'] },
},
},
},
});
---
// src/pages/preview.astro
export const prerender = false;
---
The presence of any single page with export const prerender = false is enough to trigger the internal mode flip. The Vercel build log confirms this:
[build] output: "static"
[build] mode: "server"
Steps to reproduce
- Astro 6 project with
output: 'static' and adapter: vercel().
- At least one page with
export const prerender = false (forces hybrid mode internally).
- At least one prerendered page that loads a chunk graph deep enough that one of those chunks contains a runtime
import("/_astro/<chunk>.<hash>.js") for a code-split module (e.g. a Vue component using a third-party library like glightbox via import()).
npx astro build, deploy to Vercel.
- Open the prerendered page in a browser.
What's the expected result?
Every dynamic-import() URL baked into an emitted chunk references a hash that exists in .vercel/output/static/_astro/.
Actual behavior
The prerendered HTML loads, the top-level chunk references resolve, but a runtime import() 404s:
GET /_astro/glightbox.min.B1bcp3vl.js → 404
GET /_astro/BioCardGrid.C9a1xVsr.js → 404
GET /_astro/MediaGalleryGrid.By0gxSrg.js → 404
GET /_astro/Itinerary.BhHFFt6p.js → 404
The 404 falls through to /404.html, so the browser receives text/html and reports a MIME-type error refusing the module.
Root cause (per Vercel platform team's diagnosis)
The chunk is in the deployment — under a different content hash than the runtime is asking for.
In dpl_H8pNeWLH67hfZUerbCBmT8Q5EdVz's static tree:
- The prerendered HTML for
/program/summer-abroad-guatemala-spanish-language-4wk imports a set of top-level chunks (Sections.BMGNIdqG.js, ProgramHero.DygN9__3.js, Layout.…C2l0G-nK.js, etc.). All of those exist in _astro/ and load fine, which is why the page itself renders.
- One of those chunks (or one further down the import graph) contains a baked-in
import("/_astro/glightbox.min.B1bcp3vl.js") for the runtime dynamic import. That hash points to a chunk that was never emitted by the Client pass.
- The actual emitted glightbox runtime chunk is
/_astro/glightbox.min.CSueQNvw.js. Same source module, different content hash.
Because hybrid mode runs multiple Vite passes (Server, Prerender, Client), each with its own Rollup invocation and its own content-hash table:
- Top-level chunk references that the Prerender pass bakes into the static HTML are reconciled with the Client pass's emitted filenames (so the page loads).
- Dynamic-
import() URLs baked into the chunk bodies themselves are not reconciled, so a Client-emitted chunk can still contain import() URLs computed against a different pass's hash table, pointing at chunks that were never emitted under those names.
CSS reconciliation works (stylesheets load fine). Top-level <script> references in HTML reconcile. Runtime-imported JS chunk URLs baked into chunk bodies do not.
Investigation findings (local reproduction)
1. Local build output matches the broken behavior
Running VERCEL=1 npx astro build locally:
.vercel/output/static/_astro/ contains 84 files including glightbox.min.CSueQNvw.js.
- The HTML for the affected page references top-level chunks like
Sections.IVt2RCeE.js — all present.
- That chunk imports an inner
Sections.BE_kPwOj.js — present.
- The inner chunk contains static
import("./BioCardGrid.<hash>.js") etc. — those hashes match files on disk.
The mismatch we observe in the deployment (B1bcp3vl requested, CSueQNvw on disk) is consistent with one Vite pass writing a chunk that references a hash table from a different pass — exactly the reconciliation gap described above.
2. Astro 5 comparison
The same project on Astro 5.18.1 / @astrojs/vercel v9.0.4 deploys and works correctly:
|
Astro 5 / adapter v9 |
Astro 6 / adapter v10 |
| Deployment type |
Pure static (no server function) |
Hybrid (_render function present) |
| Build pipeline |
Subpath adapter (/static) — single environment |
Multi-pass (Server / Prerender / Client) |
?dpl= on _astro JS |
No |
Yes (all assets) |
| Top-level chunks in HTML |
Resolve |
Resolve |
Dynamic import() chunks |
Resolve |
404 (hash mismatch) |
| CSS / images |
Resolve |
Resolve |
3. Ruled-out causes
- Redirect count: Project has ~1168 redirects sourced from a WordPress API; removing them entirely does not fix the 404s.
- Vite alias: The
@assets alias was added in the same commit timeline; removing it does not fix the issue.
- Vercel platform / asset upload: The Vercel platform team verified the deployment served exactly what the build pipeline produced. The mismatched hash was never written to disk by the Client pass — this is a build-pipeline bug, not a serving bug.
Related prior art
Same family of regressions in Astro 6's multi-environment build pipeline:
The reconciliation that was added for static HTML and CSS appears to not extend to dynamic-import() URLs baked into chunk bodies.
Additional context
- This regression affects any Astro 6 project that combines
output: 'static' with even a single prerender = false page (which silently flips the build to hybrid mode), and that has a chunk graph deep enough for runtime import() URLs to be baked into chunk bodies (common with Vue/React component code-splitting and third-party libs like glightbox).
- The Vercel adapter is not the source of the bug — the 404 ->
/404.html fallthrough is the standard @astrojs/vercel routes config doing the right thing for an asset that genuinely doesn't exist on disk under the requested name. The Vercel platform team has been notified and may ship a defensive measure in the adapter while the core fix is in flight.
- This issue was diagnosed in collaboration with Vercel support after they ruled out the platform side.
Link to Minimal Reproducible Example
https://where-there-be-dragons.970design.com/program/summer-abroad-guatemala-spanish-language-4wk
Participation
Astro Info
If this issue only occurs in one browser, which browser is a problem?
All
Describe the Bug
In Astro 6, when
output: 'static'is combined with at least one page that usesexport const prerender = false, Astro internally flips the build into hybrid mode. In that mode, the multiple Vite passes (Server, Prerender, Client) compute their own content-addressed hashes, and reconciliation between those passes is incomplete: dynamic-import()URLs that get baked into chunk bodies can reference hashes that were never emitted by the Client pass.The result is that the prerendered HTML loads fine — the top-level chunk references it imports were reconciled — but a runtime
import()call inside one of those chunks resolves to a/_astro/<name>.<wrongHash>.jsthat doesn't exist on disk. The request 404s, falls through to/404.html(per the Vercel adapter's standard routes), and the browser refuses the module with a MIME error because the response istext/html.This was not present in Astro 5 /
@astrojs/vercelv9 because the/staticand/serverlesssubpath adapters kept the build environments fully separate.Environment
Configuration
The presence of any single page with
export const prerender = falseis enough to trigger the internal mode flip. The Vercel build log confirms this:Steps to reproduce
output: 'static'andadapter: vercel().export const prerender = false(forces hybrid mode internally).import("/_astro/<chunk>.<hash>.js")for a code-split module (e.g. a Vue component using a third-party library likeglightboxviaimport()).npx astro build, deploy to Vercel.What's the expected result?
Every dynamic-
import()URL baked into an emitted chunk references a hash that exists in.vercel/output/static/_astro/.Actual behavior
The prerendered HTML loads, the top-level chunk references resolve, but a runtime
import()404s:The 404 falls through to
/404.html, so the browser receivestext/htmland reports a MIME-type error refusing the module.Root cause (per Vercel platform team's diagnosis)
The chunk is in the deployment — under a different content hash than the runtime is asking for.
In
dpl_H8pNeWLH67hfZUerbCBmT8Q5EdVz's static tree:/program/summer-abroad-guatemala-spanish-language-4wkimports a set of top-level chunks (Sections.BMGNIdqG.js,ProgramHero.DygN9__3.js,Layout.…C2l0G-nK.js, etc.). All of those exist in_astro/and load fine, which is why the page itself renders.import("/_astro/glightbox.min.B1bcp3vl.js")for the runtime dynamic import. That hash points to a chunk that was never emitted by the Client pass./_astro/glightbox.min.CSueQNvw.js. Same source module, different content hash.Because hybrid mode runs multiple Vite passes (Server, Prerender, Client), each with its own Rollup invocation and its own content-hash table:
import()URLs baked into the chunk bodies themselves are not reconciled, so a Client-emitted chunk can still containimport()URLs computed against a different pass's hash table, pointing at chunks that were never emitted under those names.CSS reconciliation works (stylesheets load fine). Top-level
<script>references in HTML reconcile. Runtime-imported JS chunk URLs baked into chunk bodies do not.Investigation findings (local reproduction)
1. Local build output matches the broken behavior
Running
VERCEL=1 npx astro buildlocally:.vercel/output/static/_astro/contains 84 files includingglightbox.min.CSueQNvw.js.Sections.IVt2RCeE.js— all present.Sections.BE_kPwOj.js— present.import("./BioCardGrid.<hash>.js")etc. — those hashes match files on disk.The mismatch we observe in the deployment (
B1bcp3vlrequested,CSueQNvwon disk) is consistent with one Vite pass writing a chunk that references a hash table from a different pass — exactly the reconciliation gap described above.2. Astro 5 comparison
The same project on Astro 5.18.1 /
@astrojs/vercelv9.0.4 deploys and works correctly:_renderfunction present)/static) — single environment?dpl=on_astroJSimport()chunks3. Ruled-out causes
@assetsalias was added in the same commit timeline; removing it does not fix the issue.Related prior art
Same family of regressions in Astro 6's multi-environment build pipeline:
The reconciliation that was added for static HTML and CSS appears to not extend to dynamic-
import()URLs baked into chunk bodies.Additional context
output: 'static'with even a singleprerender = falsepage (which silently flips the build to hybrid mode), and that has a chunk graph deep enough for runtimeimport()URLs to be baked into chunk bodies (common with Vue/React component code-splitting and third-party libs likeglightbox)./404.htmlfallthrough is the standard@astrojs/vercelroutes config doing the right thing for an asset that genuinely doesn't exist on disk under the requested name. The Vercel platform team has been notified and may ship a defensive measure in the adapter while the core fix is in flight.Link to Minimal Reproducible Example
https://where-there-be-dragons.970design.com/program/summer-abroad-guatemala-spanish-language-4wk
Participation