Skip to content

Commit

Permalink
feat(nuxt): allow server islands to manipulate head (#27987)
Browse files Browse the repository at this point in the history
  • Loading branch information
huang-julien authored Aug 22, 2024
1 parent f602062 commit 8730dde
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 100 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@types/node": "20.16.1",
"@types/semver": "7.5.8",
"@unhead/schema": "1.10.0",
"@unhead/vue": "^1.10.0",
"@vitejs/plugin-vue": "5.1.2",
"@vitest/coverage-v8": "2.0.5",
"@vue/test-utils": "2.4.6",
Expand Down
5 changes: 5 additions & 0 deletions packages/nuxt/src/app/components/island-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { defineAsyncComponent } from 'vue'
import { createVNode, defineComponent, onErrorCaptured } from 'vue'

import { injectHead } from '@unhead/vue'
import { createError } from '../composables/error'

// @ts-expect-error virtual file
Expand All @@ -14,6 +15,10 @@ export default defineComponent({
},
},
setup (props) {
// reset head - we don't want to have any head tags from plugin or anywhere else.
const head = injectHead()
head.headEntries().splice(0, head.headEntries().length)

const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>

if (!component) {
Expand Down
17 changes: 11 additions & 6 deletions packages/nuxt/src/app/components/nuxt-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineCom
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendResponseHeader } from 'h3'
import { useHead } from '@unhead/vue'
import { injectHead } from '@unhead/vue'
import { randomUUID } from 'uncrypto'
import { joinURL, withQuery } from 'ufo'
import type { FetchResponse } from 'ofetch'
Expand Down Expand Up @@ -96,7 +96,7 @@ export default defineComponent({
if (result.props) { toRevive.props = result.props }
if (result.slots) { toRevive.slots = result.slots }
if (result.components) { toRevive.components = result.components }

if (result.head) { toRevive.head = result.head }
nuxtApp.payload.data[key] = {
__nuxt_island: {
key,
Expand Down Expand Up @@ -158,8 +158,7 @@ export default defineComponent({
return html
})

const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
useHead(cHead)
const head = injectHead()

async function _fetchComponent (force = false) {
const key = `${props.name}_${hashId.value}`
Expand Down Expand Up @@ -199,8 +198,7 @@ export default defineComponent({
}
try {
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
cHead.value.link = res.head.link
cHead.value.style = res.head.style

ssrHTML.value = res.html.replaceAll(DATA_ISLAND_UID_RE, `data-island-uid="${uid.value}"`)
key.value++
error.value = null
Expand Down Expand Up @@ -248,6 +246,13 @@ export default defineComponent({
await loadComponents(props.source, payloads.components)
}

if (import.meta.server || nuxtApp.isHydrating) {
// re-push head into active head instance
(nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head?.forEach((h) => {
head.push(h)
})
}

return (_ctx: any, _cache: any) => {
if (!html.value || error.value) {
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
Expand Down
5 changes: 0 additions & 5 deletions packages/nuxt/src/app/plugins/revive-payload.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ if (componentIslands) {
}
return {
html: '',
state: {},
head: {
link: [],
style: [],
},
...result,
}
}
Expand Down
32 changes: 11 additions & 21 deletions packages/nuxt/src/core/runtime/nitro/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ import { stringify, uneval } from 'devalue'
import destr from 'destr'
import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo'
import { renderToString as _renderToString } from 'vue/server-renderer'
import { hash } from 'ohash'
import { propsToString, renderSSRHead } from '@unhead/ssr'
import type { HeadEntryOptions } from '@unhead/schema'
import type { Head, HeadEntryOptions } from '@unhead/schema'
import type { Link, Script, Style } from '@unhead/vue'
import { createServerHead } from '@unhead/vue'
import { createServerHead, resolveUnrefHeadInput } from '@unhead/vue'

import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig, useStorage } from 'nitro/runtime'

Expand Down Expand Up @@ -79,10 +78,7 @@ export interface NuxtIslandContext {
export interface NuxtIslandResponse {
id?: string
html: string
head: {
link: (Record<string, string>)[]
style: ({ innerHTML: string, key: string })[]
}
head: Head[]
props?: Record<string, Record<string, any>>
components?: Record<string, NuxtIslandClientResponse>
slots?: Record<string, NuxtIslandSlotResponse>
Expand Down Expand Up @@ -288,6 +284,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
const head = createServerHead({
plugins: unheadPlugins,
})

// needed for hash hydration plugin to work
const headEntryOptions: HeadEntryOptions = { mode: 'server' }
if (!isRenderingIsland) {
Expand Down Expand Up @@ -394,7 +391,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
}

// 2. Styles
head.push({ style: inlinedStyles })
if (inlinedStyles.length) {
head.push({ style: inlinedStyles })
}
if (!isRenderingIsland || import.meta.dev) {
const link: Link[] = []
for (const style in styles) {
Expand All @@ -411,7 +410,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
link.push({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
}
}
head.push({ link }, headEntryOptions)
if (link.length) {
head.push({ link }, headEntryOptions)
}
}

if (!NO_SCRIPTS && !isRenderingIsland) {
Expand Down Expand Up @@ -460,20 +461,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse

// Response for component islands
if (isRenderingIsland && islandContext) {
const islandHead: NuxtIslandResponse['head'] = {
link: [],
style: [],
}
for (const tag of await head.resolveTags()) {
if (tag.tag === 'link') {
islandHead.link.push({ key: 'island-link-' + hash(tag.props), ...tag.props })
} else if (tag.tag === 'style' && tag.innerHTML) {
islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
}
}
const islandResponse: NuxtIslandResponse = {
id: islandContext.id,
head: islandHead,
head: (head.headEntries().map(h => resolveUnrefHeadInput(h.input) as Head)),
html: getServerComponentHTML(_rendered.html),
components: getClientIslandResponse(ssrContext),
slots: getSlotIslandResponse(ssrContext),
Expand Down
10 changes: 4 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8730dde

Please sign in to comment.