mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
feat(nuxt): allow server islands to manipulate head (#27987)
This commit is contained in:
parent
e4569bbdc6
commit
3f1db54b80
@ -57,6 +57,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",
|
||||
|
@ -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
|
||||
@ -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) {
|
||||
|
@ -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'
|
||||
@ -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,
|
||||
@ -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}`
|
||||
@ -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
|
||||
@ -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')]
|
||||
|
@ -31,11 +31,6 @@ if (componentIslands) {
|
||||
}
|
||||
return {
|
||||
html: '',
|
||||
state: {},
|
||||
head: {
|
||||
link: [],
|
||||
style: [],
|
||||
},
|
||||
...result,
|
||||
}
|
||||
}
|
||||
|
@ -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, useRuntimeConfig, useStorage } from '#internal/nitro'
|
||||
import { useNitroApp } from '#internal/nitro/app'
|
||||
@ -80,10 +79,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>
|
||||
@ -289,6 +285,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) {
|
||||
@ -395,7 +392,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
}
|
||||
|
||||
// 2. Styles
|
||||
if (inlinedStyles.length) {
|
||||
head.push({ style: inlinedStyles })
|
||||
}
|
||||
if (!isRenderingIsland || import.meta.dev) {
|
||||
const link: Link[] = []
|
||||
for (const style in styles) {
|
||||
@ -412,8 +411,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
link.push({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
|
||||
}
|
||||
}
|
||||
if (link.length) {
|
||||
head.push({ link }, headEntryOptions)
|
||||
}
|
||||
}
|
||||
|
||||
if (!NO_SCRIPTS && !isRenderingIsland) {
|
||||
// 3. Resource Hints
|
||||
@ -478,20 +479,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(htmlContext.body),
|
||||
components: getClientIslandResponse(ssrContext),
|
||||
slots: getSlotIslandResponse(ssrContext),
|
||||
|
1049
pnpm-lock.yaml
1049
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,8 @@ import { join, normalize } from 'pathe'
|
||||
import { $fetch, createPage, fetch, isDev, setup, startServer, url, useTestContext } from '@nuxt/test-utils/e2e'
|
||||
import { $fetchComponent } from '@nuxt/test-utils/experimental'
|
||||
|
||||
import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils'
|
||||
import { resolveUnrefHeadInput } from '@unhead/vue'
|
||||
import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage, resolveHead } from './utils'
|
||||
|
||||
import type { NuxtIslandResponse } from '#app'
|
||||
|
||||
@ -135,7 +136,7 @@ describe('pages', () => {
|
||||
// should apply attributes to client-only components
|
||||
expect(html).toContain('<div style="color:red;" class="client-only"></div>')
|
||||
// should render server-only components
|
||||
expect(html.replace(/ data-island-uid="[^"]*"/, '')).toContain('<div class="server-only" style="background-color:gray;"> server-only component <div> server-only component child (non-server-only) </div></div>')
|
||||
expect(html.replaceAll(/ data-island-uid="[^"]*"/g, '')).toContain('<div class="server-only" style="background-color:gray;"> server-only component <div> server-only component child (non-server-only) </div></div>')
|
||||
// should register global components automatically
|
||||
expect(html).toContain('global component registered automatically')
|
||||
expect(html).toContain('global component via suffix')
|
||||
@ -1932,6 +1933,12 @@ describe('server components/islands', () => {
|
||||
await page.close()
|
||||
})
|
||||
|
||||
it('/server-page', async () => {
|
||||
const html = await $fetch<string>('/server-page')
|
||||
// test island head
|
||||
expect(html).toContain('<meta name="author" content="Nuxt">')
|
||||
})
|
||||
|
||||
it.skipIf(isDev)('should allow server-only components to set prerender hints', async () => {
|
||||
// @ts-expect-error ssssh! untyped secret property
|
||||
const publicDir = useTestContext().nuxt._nitro.options.output.publicDir
|
||||
@ -2141,15 +2148,15 @@ describe('component islands', () => {
|
||||
|
||||
result.html = result.html.replace(/ data-island-uid="[^"]*"/g, '')
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/RouteComponent')))
|
||||
result.head = resolveHead(result.head).map(h => ({
|
||||
...h,
|
||||
link: h.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/RouteComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)),
|
||||
})).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length))
|
||||
}
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"head": [],
|
||||
"html": "<pre data-island-uid> Route: /foo
|
||||
</pre>",
|
||||
}
|
||||
@ -2163,15 +2170,15 @@ describe('component islands', () => {
|
||||
}),
|
||||
}))
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/LongAsyncComponent')))
|
||||
result.head = resolveHead(result.head).map(h => ({
|
||||
...h,
|
||||
link: h.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/LongAsyncComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)),
|
||||
})).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length))
|
||||
}
|
||||
result.html = result.html.replaceAll(/ (data-island-uid|data-island-component)="([^"]*)"/g, '')
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"head": [],
|
||||
"html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"><!--teleport start--><!--teleport end--></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--></div>",
|
||||
"slots": {
|
||||
"default": {
|
||||
@ -2221,7 +2228,10 @@ describe('component islands', () => {
|
||||
}),
|
||||
}))
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/AsyncServerComponent')))
|
||||
result.head = result.head.map(h => ({
|
||||
...h,
|
||||
link: h.link?.filter(l => typeof l.href === 'string' && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */ && (!l.href.startsWith('_nuxt/components/islands/') || l.href.includes('AsyncServerComponent'))),
|
||||
})).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length))
|
||||
}
|
||||
result.props = {}
|
||||
result.components = {}
|
||||
@ -2231,10 +2241,7 @@ describe('component islands', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"components": {},
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"head": [],
|
||||
"html": "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div>",
|
||||
"props": {},
|
||||
"slots": {},
|
||||
@ -2246,7 +2253,10 @@ describe('component islands', () => {
|
||||
it('render server component with selective client hydration', async () => {
|
||||
const result = await $fetch<NuxtIslandResponse>('/__nuxt_island/ServerWithClient')
|
||||
if (isDev()) {
|
||||
result.head.link = result.head.link.filter(l => !l.href!.includes('@nuxt+ui-templates') && (l.href!.startsWith('_nuxt/components/islands/') && l.href!.includes('_nuxt/components/islands/AsyncServerComponent')))
|
||||
result.head = resolveHead(result.head).map(h => ({
|
||||
...h,
|
||||
link: h.link?.filter(l => typeof l.href !== 'string' || (!l.href.includes('_nuxt/components/islands/LongAsyncComponent') && !l.href.includes('PureComponent') /* TODO: fix dev bug triggered by previous fetch of /islands */)),
|
||||
})).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length))
|
||||
}
|
||||
const { components } = result
|
||||
result.components = {}
|
||||
@ -2258,10 +2268,7 @@ describe('component islands', () => {
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"components": {},
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"head": [],
|
||||
"html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
|
||||
"slots": {},
|
||||
}
|
||||
@ -2290,41 +2297,52 @@ describe('component islands', () => {
|
||||
|
||||
if (isDev()) {
|
||||
const fixtureDir = normalize(fileURLToPath(new URL('./fixtures/basic', import.meta.url)))
|
||||
for (const link of result.head.link) {
|
||||
link.href = link.href!.replace(fixtureDir, '/<rootDir>').replaceAll('//', '/')
|
||||
link.key = link.key!.replace(/-[a-z0-9]+$/i, '')
|
||||
for (const head of result.head) {
|
||||
for (const key in head) {
|
||||
if (key === 'link') {
|
||||
head[key] = head[key]?.map((h) => {
|
||||
if (h.href) {
|
||||
h.href = resolveUnrefHeadInput(h.href).replace(fixtureDir, '/<rootDir>').replaceAll('//', '/')
|
||||
}
|
||||
return h
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
result.head.link.sort((a, b) => b.href!.localeCompare(a.href!))
|
||||
}
|
||||
|
||||
// TODO: fix rendering of styles in webpack
|
||||
if (!isDev() && !isWebpack) {
|
||||
expect(normaliseIslandResult(result).head).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"link": [],
|
||||
"style": [
|
||||
{
|
||||
"innerHTML": "pre[data-v-xxxxx]{color:blue}",
|
||||
"key": "island-style",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
]
|
||||
`)
|
||||
} else if (isDev() && !isWebpack) {
|
||||
// TODO: resolve dev bug triggered by earlier fetch of /vueuse-head page
|
||||
// https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/runtime/nitro/renderer.ts#L139
|
||||
result.head.link = result.head.link.filter(h => !h.href!.includes('SharedComponent'))
|
||||
result.head = resolveHead(result.head).map(h => ({
|
||||
...h,
|
||||
link: h.link?.filter(l => typeof l.href !== 'string' || !l.href.includes('SharedComponent')),
|
||||
})).filter(h => Object.values(h).some(h => !Array.isArray(h) || h.length))
|
||||
|
||||
expect(result.head).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"link": [
|
||||
{
|
||||
"href": "/_nuxt/components/islands/PureComponent.vue?vue&type=style&index=0&scoped=c0c0cf89&lang.css",
|
||||
"key": "island-link",
|
||||
"rel": "stylesheet",
|
||||
},
|
||||
],
|
||||
"style": [],
|
||||
}
|
||||
},
|
||||
]
|
||||
`)
|
||||
}
|
||||
|
||||
@ -2671,14 +2689,21 @@ describe('Node.js compatibility for client-side', () => {
|
||||
function normaliseIslandResult (result: NuxtIslandResponse) {
|
||||
return {
|
||||
...result,
|
||||
head: {
|
||||
...result.head,
|
||||
style: result.head.style.map(s => ({
|
||||
...s,
|
||||
innerHTML: (s.innerHTML || '').replace(/data-v-[a-z0-9]+/, 'data-v-xxxxx').replace(/\.[a-zA-Z0-9]+\.svg/, '.svg'),
|
||||
key: s.key.replace(/-[a-z0-9]+$/i, ''),
|
||||
})),
|
||||
},
|
||||
head: result.head.map((h) => {
|
||||
if (h.style) {
|
||||
for (const style of h.style) {
|
||||
if (typeof style !== 'string') {
|
||||
if (style.innerHTML) {
|
||||
style.innerHTML = (style.innerHTML as string).replace(/data-v-[a-z0-9]+/g, 'data-v-xxxxx')
|
||||
}
|
||||
if (style.key) {
|
||||
style.key = style.key.replace(/-[a-z0-9]+$/i, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"209k"`)
|
||||
|
||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1351k"`)
|
||||
@ -72,7 +72,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output-inline/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"534k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"533k"`)
|
||||
|
||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"82.1k"`)
|
||||
|
33
test/fixtures/basic/pages/index.vue
vendored
33
test/fixtures/basic/pages/index.vue
vendored
@ -79,6 +79,11 @@
|
||||
style="color: red;"
|
||||
class="client-only"
|
||||
/>
|
||||
<NuxtIsland
|
||||
ref="island"
|
||||
name="AsyncServerComponent"
|
||||
:props="{ count: 34 }"
|
||||
/>
|
||||
<ServerOnlyComponent
|
||||
class="server-only"
|
||||
style="background-color: gray;"
|
||||
@ -96,9 +101,10 @@
|
||||
import { setupDevtoolsPlugin } from '@vue/devtools-api'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import { importedRE, importedValue } from '~/some-exports'
|
||||
import type { NuxtIsland, ServerOnlyComponent } from '#build/components'
|
||||
|
||||
setupDevtoolsPlugin({}, () => {}) as any
|
||||
|
||||
const island = ref<InstanceType<typeof ServerOnlyComponent>>()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const someValue = useState('val', () => 1)
|
||||
@ -107,7 +113,30 @@ const NestedCounter = resolveComponent('NestedCounter')
|
||||
if (!NestedCounter) {
|
||||
throw new Error('Component not found')
|
||||
}
|
||||
|
||||
useHead({
|
||||
meta: [
|
||||
{
|
||||
name: 'author',
|
||||
content: 'Nuxt',
|
||||
key: 'testkey',
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
content: 'Nuxt',
|
||||
key: 'testkey',
|
||||
},
|
||||
],
|
||||
script: [
|
||||
{
|
||||
innerHTML: 'console.log("my script")',
|
||||
key: 'my-script',
|
||||
},
|
||||
{
|
||||
innerHTML: 'console.log("my script")',
|
||||
key: 'my-script',
|
||||
},
|
||||
],
|
||||
}, { key: 'testkey' })
|
||||
definePageMeta({
|
||||
alias: '/some-alias',
|
||||
other: ref('test'),
|
||||
|
12
test/fixtures/basic/pages/server-page.server.vue
vendored
12
test/fixtures/basic/pages/server-page.server.vue
vendored
@ -8,3 +8,15 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useHead({
|
||||
title: 'Server Page',
|
||||
meta: [
|
||||
{
|
||||
name: 'author',
|
||||
content: 'Nuxt',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
@ -5,6 +5,8 @@ import { parse } from 'devalue'
|
||||
import { reactive, ref, shallowReactive, shallowRef } from 'vue'
|
||||
import { createError } from 'h3'
|
||||
import { getBrowser, url, useTestContext } from '@nuxt/test-utils/e2e'
|
||||
import type { Head } from '@unhead/vue'
|
||||
import { resolveUnrefHeadInput } from '@unhead/vue'
|
||||
|
||||
export const isRenderingJson = process.env.TEST_PAYLOAD !== 'js'
|
||||
|
||||
@ -126,3 +128,7 @@ export function parseData (html: string) {
|
||||
attrs: _attrs,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveHead (head: Head[]) {
|
||||
return head.map(i => resolveUnrefHeadInput(i) as Head)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user