Skip to content

Astro 6 hybrid build emits dynamic-import URLs with chunk hashes that don't match emitted chunks (runtime 404 on /_astro/*.js) #16520

@dividemysky

Description

@dividemysky

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

  1. Astro 6 project with output: 'static' and adapter: vercel().
  2. At least one page with export const prerender = false (forces hybrid mode internally).
  3. 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()).
  4. npx astro build, deploy to Vercel.
  5. 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

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

Labels

- P4: importantViolate documented behavior or significantly impacts performance (priority)

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions