🐛 The bug
The module's useHead replacement (src/runtime/composables/head.ts) never resolves reactive input. All of its activeHead.push() / patch() sites hand the raw object to unhead, while @unhead/vue's own clientUseHead runs walkResolver(input, VueResolver) inside a watchEffect.
This only bites on the client: @unhead/vue/server creates the head with propResolvers: [VueResolver], so refs resolve inside unhead during SSR. The client head has no prop resolver, so resolution exists only in the composable this module replaces.
Two consequences:
-
Crash — a ref on innerHTML/textContent of a style/script reaches unhead's normalizeProps, which JSON-stringifies object values:
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'ComputedRefImpl'
| property 'dep' -> object with constructor 'Dep'
--- property 'computed' closes the circle
at JSON.stringify (<anonymous>)
at normalizeProps (unhead.js:148:28)
at normalizeTag (unhead.js:175:15)
at normalizeEntryToTags (unhead.js:209:56)
at resolveTags (unhead.js:362:16)
at _renderDOMHead (unhead.js:183:21)
It is thrown inside the debounced _renderDOMHead, so the whole head render is abandoned: the tag never lands in the DOM, and it can abort router start.
-
Silent, for every other prop: the ref object is used instead of its .value, and since there is no watchEffect, the tag never updates when the ref changes. Client-side reactive head does not work, though unhead v3 documents it as supported.
@nuxt/ui triggers case 1 out of the box: its colors plugin calls useHead({ style: [{ innerHTML: root, id: 'nuxt-ui-colors' }] }) where root is a computed. Any @nuxtjs/ionic + @nuxt/ui app loses its theme colors style. @nuxtjs/i18n's useLocaleHead() and @nuxtjs/seo hit case 2.
🛠️ To reproduce
https://github.com/Carpediem94/nuxt-ionic-head-repro
🌈 Expected behaviour
Reactive input resolved before being pushed to unhead, and kept in sync as it changes, matching the behaviour of @unhead/vue's useHead.
ℹ️ Additional context
@nuxtjs/ionic 1.0.2, nuxt 4.5.2, @unhead/vue 3.3.1, @nuxt/ui 4.10.0.
Same error class as unjs/unhead#869, but that fix only covered the SSR streaming serializer.
🐛 The bug
The module's
useHeadreplacement (src/runtime/composables/head.ts) never resolves reactive input. All of itsactiveHead.push()/patch()sites hand the raw object to unhead, while@unhead/vue's ownclientUseHeadrunswalkResolver(input, VueResolver)inside awatchEffect.This only bites on the client:
@unhead/vue/servercreates the head withpropResolvers: [VueResolver], so refs resolve inside unhead during SSR. The client head has no prop resolver, so resolution exists only in the composable this module replaces.Two consequences:
Crash — a ref on
innerHTML/textContentof astyle/scriptreaches unhead'snormalizeProps, which JSON-stringifies object values:It is thrown inside the debounced
_renderDOMHead, so the whole head render is abandoned: the tag never lands in the DOM, and it can abort router start.Silent, for every other prop: the ref object is used instead of its
.value, and since there is nowatchEffect, the tag never updates when the ref changes. Client-side reactive head does not work, though unhead v3 documents it as supported.@nuxt/uitriggers case 1 out of the box: its colors plugin callsuseHead({ style: [{ innerHTML: root, id: 'nuxt-ui-colors' }] })whererootis acomputed. Any@nuxtjs/ionic+@nuxt/uiapp loses its theme colors style.@nuxtjs/i18n'suseLocaleHead()and@nuxtjs/seohit case 2.🛠️ To reproduce
https://github.com/Carpediem94/nuxt-ionic-head-repro
🌈 Expected behaviour
Reactive input resolved before being pushed to unhead, and kept in sync as it changes, matching the behaviour of
@unhead/vue'suseHead.ℹ️ Additional context
@nuxtjs/ionic1.0.2,nuxt4.5.2,@unhead/vue3.3.1,@nuxt/ui4.10.0.Same error class as unjs/unhead#869, but that fix only covered the SSR streaming serializer.