Description
With Nuxt 4.5.0, Nuxt migrated to Unhead v3 (renderSSRHead is synchronous). @nuxt/ui@4.10.0 still depends on @unhead/vue@^2.1.15, which pulls Unhead v2 into the install tree.
In a Nuxt 4.5 + Nuxt UI app, Nitro can trace both versions into the server output:
.output/server/node_modules/.nitro/unhead@2.1.16/
.output/server/node_modules/.nitro/unhead@3.2.2/
.output/server/node_modules/unhead -> .nitro/unhead@2.1.16 # wrong
Nuxt's SSR renderer imports bare unhead/server and calls renderSSRHead without await (v3 API). Resolving v2's async renderSSRHead means the Promise is destructured instead of the head result, so all SSR head tags are dropped — including the client entry <script type="module" src="/_nuxt/...">.
Impact
- Production builds serve HTML with no client JS (hydration never starts)
- Apps look “SSR-only” / non-interactive
- Playwright/
@nuxt/test-utils e2e with { waitUntil: 'hydration' } times out (window.useNuxtApp never appears)
This is not limited to tests; it affects the production Nitro output.
Environment
nuxt: ^4.5.0 (resolved 4.5.0)
@nuxt/ui: 4.10.0 (latest at report time)
- Package manager: bun
@nuxt/ui declares: "@unhead/vue": "^2.1.15"
Reproduction
- Create/use a Nuxt 4.5 app with
@nuxt/ui
- Do not override
@unhead/vue / unhead
nuxt build (or e2e build via @nuxt/test-utils)
- Inspect
.output/server/node_modules/unhead (symlink target) and curl /
- Observe missing
/_nuxt/ entry script in HTML when the v2 symlink wins
Workaround
Force a single Unhead v3 resolution (works for us with bun):
{
"overrides": {
"@unhead/vue": "^3.2.2",
"unhead": "^3.2.2"
}
}
After that, .output/server/node_modules/unhead resolves to 3.2.2, entry scripts return, and hydration works again.
Expected
@nuxt/ui should depend on (or peer on) @unhead/vue v3 aligned with Nuxt 4.5+, so consumers do not need overrides.
Nuxt UI already appears runtime-compatible with Unhead v3 in places (e.g. nuxt/ui#6658 avoided v2-only hookOnce), so this looks primarily like a dependency pin that still forces Unhead v2 into the tree.
Additional context
Description
With Nuxt 4.5.0, Nuxt migrated to Unhead v3 (
renderSSRHeadis synchronous).@nuxt/ui@4.10.0still depends on@unhead/vue@^2.1.15, which pulls Unhead v2 into the install tree.In a Nuxt 4.5 + Nuxt UI app, Nitro can trace both versions into the server output:
Nuxt's SSR renderer imports bare
unhead/serverand callsrenderSSRHeadwithoutawait(v3 API). Resolving v2's asyncrenderSSRHeadmeans the Promise is destructured instead of the head result, so all SSR head tags are dropped — including the client entry<script type="module" src="/_nuxt/...">.Impact
@nuxt/test-utilse2e with{ waitUntil: 'hydration' }times out (window.useNuxtAppnever appears)This is not limited to tests; it affects the production Nitro output.
Environment
nuxt:^4.5.0(resolved4.5.0)@nuxt/ui:4.10.0(latest at report time)@nuxt/uideclares:"@unhead/vue": "^2.1.15"Reproduction
@nuxt/ui@unhead/vue/unheadnuxt build(or e2e build via@nuxt/test-utils).output/server/node_modules/unhead(symlink target) and curl//_nuxt/entry script in HTML when the v2 symlink winsWorkaround
Force a single Unhead v3 resolution (works for us with bun):
{ "overrides": { "@unhead/vue": "^3.2.2", "unhead": "^3.2.2" } }After that,
.output/server/node_modules/unheadresolves to3.2.2, entry scripts return, and hydration works again.Expected
@nuxt/uishould depend on (or peer on)@unhead/vuev3 aligned with Nuxt 4.5+, so consumers do not need overrides.Nuxt UI already appears runtime-compatible with Unhead v3 in places (e.g. nuxt/ui#6658 avoided v2-only
hookOnce), so this looks primarily like a dependency pin that still forces Unhead v2 into the tree.Additional context