diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 1db032f8df3..e5efba4141d 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -44,7 +44,8 @@ "@nuxt/vite-builder": "3.0.0-rc.13", "@vue/reactivity": "^3.2.45", "@vue/shared": "^3.2.45", - "@vueuse/head": "~1.0.0-rc.14", + "@vueuse/head": "^1.0.0", + "@unhead/ssr": "^0.6.0", "chokidar": "^3.5.3", "cookie-es": "^0.5.0", "defu": "^6.1.0", diff --git a/packages/nuxt/src/head/runtime/composables.ts b/packages/nuxt/src/head/runtime/composables.ts index d404b4f2f09..fbbc37a03cb 100644 --- a/packages/nuxt/src/head/runtime/composables.ts +++ b/packages/nuxt/src/head/runtime/composables.ts @@ -1,5 +1,5 @@ -import type { MetaObject } from '@nuxt/schema' -import type { MaybeComputedRef } from '@vueuse/head' +import type { UseHeadInput } from '@vueuse/head' +import type { HeadAugmentations } from '@nuxt/schema' import { useNuxtApp } from '#app' /** @@ -9,12 +9,12 @@ import { useNuxtApp } from '#app' * Alternatively, for reactive meta state, you can pass in a function * that returns a meta object. */ -export function useHead (meta: MaybeComputedRef) { +export function useHead (meta: UseHeadInput) { useNuxtApp()._useHead(meta) } // TODO: remove useMeta support when Nuxt 3 is stable /** @deprecated Please use new `useHead` composable instead */ -export function useMeta (meta: MaybeComputedRef) { +export function useMeta (meta: UseHeadInput) { return useHead(meta) } diff --git a/packages/nuxt/src/head/runtime/index.ts b/packages/nuxt/src/head/runtime/index.ts index 6c8caa85f6a..a10cf33a400 100644 --- a/packages/nuxt/src/head/runtime/index.ts +++ b/packages/nuxt/src/head/runtime/index.ts @@ -1,2 +1,5 @@ +import type { UseHeadInput } from '@vueuse/head' +import type { HeadAugmentations } from '@nuxt/schema' export * from './composables' -export type { MetaObject } from '@nuxt/schema' + +export type MetaObject = UseHeadInput diff --git a/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts b/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts index 1b2c412ffc2..2d128bc4d9c 100644 --- a/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts +++ b/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts @@ -1,64 +1,49 @@ -import type { HeadEntryOptions, MaybeComputedRef } from '@vueuse/head' -import { createHead, renderHeadToString } from '@vueuse/head' -import { onBeforeUnmount, getCurrentInstance } from 'vue' -import type { MetaObject } from '@nuxt/schema' -import { defineNuxtPlugin, useRouter } from '#app' +import { createHead, useHead } from '@vueuse/head' +import { defineNuxtPlugin } from '#app' // @ts-expect-error untyped import { appHead } from '#build/nuxt.config.mjs' export default defineNuxtPlugin((nuxtApp) => { const head = createHead() - head.addEntry(appHead, { resolved: true }) + // we can server side render the head without having the client hydrate it + if (process.server) { + head.push(appHead, { mode: 'server' }) + } + + if (process.client && nuxtApp.ssrContext?.noSSR) { + head.push(appHead) + } nuxtApp.vueApp.use(head) if (process.client) { // pause dom updates until page is ready and between page transitions let pauseDOMUpdates = true - head.hooks['before:dom'].push(() => !pauseDOMUpdates) - nuxtApp.hooks.hookOnce('app:mounted', () => { + const unpauseDom = () => { pauseDOMUpdates = false - head.updateDOM() - - // start pausing DOM updates when route changes (trigger immediately) - useRouter().beforeEach(() => { - pauseDOMUpdates = true - }) - // watch for new route before unpausing dom updates (triggered after suspense resolved) - useRouter().afterEach(() => { - // only if we have paused (clicking on a link to the current route triggers this) - if (pauseDOMUpdates) { - pauseDOMUpdates = false - head.updateDOM() - } - }) - }) - } - - nuxtApp._useHead = (_meta: MaybeComputedRef, options: HeadEntryOptions) => { - if (process.server) { - head.addEntry(_meta, options) - return + // triggers dom update + head.hooks.callHook('entries:updated', head) } - - const cleanUp = head.addReactiveEntry(_meta, options) - - const vm = getCurrentInstance() - if (!vm) { return } - - onBeforeUnmount(() => { - cleanUp() - head.updateDOM() + head.hooks.hook('dom:beforeRender', (context) => { + context.shouldRender = !pauseDOMUpdates }) + nuxtApp.hooks.hook('page:start', () => { pauseDOMUpdates = true }) + // watch for new route before unpausing dom updates (triggered after suspense resolved) + nuxtApp.hooks.hook('page:finish', unpauseDom) + nuxtApp.hooks.hook('app:mounted', unpauseDom) } + nuxtApp._useHead = useHead + if (process.server) { nuxtApp.ssrContext!.renderMeta = async () => { - const meta = await renderHeadToString(head) + const { renderSSRHead } = await import('@unhead/ssr') + const meta = await renderSSRHead(head) return { ...meta, // resolves naming difference with NuxtMeta and @vueuse/head + bodyScriptsPrepend: meta.bodyTagsOpen, bodyScripts: meta.bodyTags } } diff --git a/packages/nuxt/src/head/runtime/mixin-plugin.ts b/packages/nuxt/src/head/runtime/mixin-plugin.ts index 906211527be..a5efd972973 100644 --- a/packages/nuxt/src/head/runtime/mixin-plugin.ts +++ b/packages/nuxt/src/head/runtime/mixin-plugin.ts @@ -15,6 +15,8 @@ const metaMixin = { ? () => options.head(nuxtApp) : options.head + console.log('mixin', source) + useHead(source) } } diff --git a/packages/schema/build.config.ts b/packages/schema/build.config.ts index 6ae914a75db..7c23363f783 100644 --- a/packages/schema/build.config.ts +++ b/packages/schema/build.config.ts @@ -22,7 +22,7 @@ export default defineBuildConfig({ 'vue-meta', 'vue-router', 'vue-bundle-renderer', - '@vueuse/head', + '@unhead/schema', 'vue', 'hookable', 'nitropack', diff --git a/packages/schema/package.json b/packages/schema/package.json index 21752708542..8dbca7db295 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -17,9 +17,9 @@ "@types/lodash.template": "^4", "@types/semver": "^7", "@vitejs/plugin-vue": "^3.2.0", - "@vueuse/head": "~1.0.0-rc.14", "nitropack": "^0.6.1", "unbuild": "latest", + "@unhead/schema": "^0.6.0", "vite": "~3.2.3" }, "dependencies": { diff --git a/packages/schema/src/types/meta.ts b/packages/schema/src/types/meta.ts index d79bfe55b72..17cf81eb300 100644 --- a/packages/schema/src/types/meta.ts +++ b/packages/schema/src/types/meta.ts @@ -1,4 +1,4 @@ -import type { HeadObjectPlain, HeadObject } from '@vueuse/head' +import type { Head } from '@unhead/schema' export interface HeadAugmentations { // runtime type modifications @@ -12,7 +12,7 @@ export interface HeadAugmentations { bodyAttrs?: {} } -export type MetaObjectRaw = HeadObjectPlain +export type MetaObjectRaw = Head export type AppHeadMetaObject = MetaObjectRaw & { /** @@ -29,70 +29,3 @@ export type AppHeadMetaObject = MetaObjectRaw & { */ viewport?: string } - -export interface MetaObject { - /** - * The HTML element defines the document's title that is shown in a browser's title bar or a page's tab. - * It only contains text; tags within the element are ignored. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title - */ - title?: HeadObject<HeadAugmentations>['title'] - /** - * Generate the title from a template. - */ - titleTemplate?: HeadObject<HeadAugmentations>['titleTemplate'] - /** - * The <base> HTML element specifies the base URL to use for all relative URLs in a document. - * There can be only one <base> element in a document. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base - */ - base?: HeadObject<HeadAugmentations>['base'] - /** - * The <link> HTML element specifies relationships between the current document and an external resource. - * This element is most commonly used to link to stylesheets, but is also used to establish site icons - * (both "favicon" style icons and icons for the home screen and apps on mobile devices) among other things. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as - */ - link?: HeadObject<HeadAugmentations>['link'] - /** - * The <meta> element represents metadata that cannot be expressed in other HTML elements, like <link> or <script>. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta - */ - meta?: HeadObject<HeadAugmentations>['meta'] - /** - * The <style> HTML element contains style information for a document, or part of a document. - * It contains CSS, which is applied to the contents of the document containing the <style> element. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style - */ - style?: HeadObject<HeadAugmentations>['style'] - /** - * The <script> HTML element is used to embed executable code or data; this is typically used to embed or refer to JavaScript code. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script - */ - script?: HeadObject<HeadAugmentations>['script'] - /** - * The <noscript> HTML element defines a section of HTML to be inserted if a script type on the page is unsupported - * or if scripting is currently turned off in the browser. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript - */ - noscript?: HeadObject<HeadAugmentations>['noscript'] - /** - * Attributes for the <html> HTML element. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html - */ - htmlAttrs?: HeadObject<HeadAugmentations>['htmlAttrs'] - /** - * Attributes for the <body> HTML element. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body - */ - bodyAttrs?: HeadObject<HeadAugmentations>['bodyAttrs'] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3c0db84163..654550c2523 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -417,9 +417,10 @@ importers: '@nuxt/vite-builder': workspace:* '@types/fs-extra': ^9.0.13 '@types/hash-sum': ^1.0.0 + '@unhead/ssr': ^0.6.0 '@vue/reactivity': ^3.2.45 '@vue/shared': ^3.2.45 - '@vueuse/head': ~1.0.0-rc.14 + '@vueuse/head': ^1.0.0 chokidar: ^3.5.3 cookie-es: ^0.5.0 defu: ^6.1.0 @@ -462,9 +463,10 @@ importers: '@nuxt/telemetry': 2.1.6 '@nuxt/ui-templates': 0.4.0 '@nuxt/vite-builder': link:../vite + '@unhead/ssr': 0.6.0 '@vue/reactivity': 3.2.45 '@vue/shared': 3.2.45 - '@vueuse/head': 1.0.0-rc.14_vue@3.2.45 + '@vueuse/head': 1.0.0_vue@3.2.45 chokidar: 3.5.3 cookie-es: 0.5.0 defu: 6.1.0 @@ -508,8 +510,8 @@ importers: specifiers: '@types/lodash.template': ^4 '@types/semver': ^7 + '@unhead/schema': ^0.6.0 '@vitejs/plugin-vue': ^3.2.0 - '@vueuse/head': ~1.0.0-rc.14 c12: ^0.2.13 create-require: ^1.1.1 defu: ^6.1.0 @@ -541,8 +543,8 @@ importers: devDependencies: '@types/lodash.template': 4.5.1 '@types/semver': 7.3.12 + '@unhead/schema': 0.6.0 '@vitejs/plugin-vue': 3.2.0_vite@3.2.3 - '@vueuse/head': 1.0.0-rc.14 nitropack: 0.6.1 unbuild: 0.9.4 vite: 3.2.3 @@ -2083,6 +2085,51 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /@unhead/dom/0.5.1: + resolution: {integrity: sha512-cdRzGbZVWTgbwl9HiG3RZzzPThXmhj5afGB2BLRwbE+3IiwqUpMjL6v8bDjE5qttvH4YrK9AD9O8fFP9XyZQpg==} + dependencies: + '@unhead/schema': 0.5.1 + dev: false + + /@unhead/schema/0.5.1: + resolution: {integrity: sha512-Wk8v18jj3PwTFSKai8YZ1ObieBQ9N2pJUvnsy6JhwCSJnLA7e0GrSP/zuluq3GZzyLNJgTr19BDmHgpDPlN61g==} + dependencies: + '@zhead/schema': 1.0.0-beta.13 + hookable: 5.4.1 + dev: false + + /@unhead/schema/0.6.0: + resolution: {integrity: sha512-NgWpul0rZrG28MEswQYVBAL3XJrFXgkF5WqdPv5/W0597JZJ4HgtbFAj5VxSQbOVomx2s+WldViMMSwCMT69GA==} + dependencies: + '@zhead/schema': 1.0.0-beta.13 + hookable: 5.4.1 + + /@unhead/ssr/0.5.1: + resolution: {integrity: sha512-F8DhVWMlfKfPvtnpPmVjVXF0HndA/ZShGzaIoCQEJt3Cfr/q4AsdvCiGreI3D4MHB3IXwXPSReV8WQenfo3Z1g==} + dependencies: + '@unhead/schema': 0.5.1 + dev: false + + /@unhead/ssr/0.6.0: + resolution: {integrity: sha512-X084RYGOywrolitLlcAllbA2JNoHyIGyREXWrvtOe2pL619o0/SQhuXFx9K2a9ikIm59m/6gwjjNGIWQSYG6gQ==} + dependencies: + '@unhead/schema': 0.6.0 + dev: false + + /@unhead/vue/0.5.1_vue@3.2.45: + resolution: {integrity: sha512-s4y4uj3NMqaUs0K+WQXbWGj/2+Glk/DEJ9yeJOcJIiro/+IhUMByD71jyCM43Xn8YBPy14VY/ZYb9ZFU2WCZgw==} + peerDependencies: + vue: '>=2.7 || >=3' + dependencies: + '@unhead/dom': 0.5.1 + '@unhead/schema': 0.5.1 + '@vueuse/shared': 9.5.0_vue@3.2.45 + unhead: 0.5.1 + vue: 3.2.45 + transitivePeerDependencies: + - '@vue/composition-api' + dev: false + /@unocss/astro/0.45.25: resolution: {integrity: sha512-TxFxESIvSZtaVIRP/cDyrwnYlHz78mG9ohhAhI0adLGi0v0yBwoGJ1krlqxAuTzhV191InTlwD/C5QevTsf86Q==} dependencies: @@ -2546,26 +2593,14 @@ packages: - vue dev: true - /@vueuse/head/1.0.0-rc.14: - resolution: {integrity: sha512-3DtOfSE1141IKPIq4AR5UXQZPWQFSd7E5f3M+HkBRyxWsyxbNBBmK5hqkSYc2ENoFXa3xPhLYZXJPKuxqfJmiA==} + /@vueuse/head/1.0.0_vue@3.2.45: + resolution: {integrity: sha512-wighjD6iLxEitpg6EDeS5dGDB9tcOSMhpblrAOKR6qBP93U3cjG72n0LhlBUD9miu41lNxXFVGHgSc6BVJ9BMg==} peerDependencies: vue: '>=2.7 || >=3' dependencies: - '@vueuse/shared': 9.4.0 - '@zhead/schema': 0.9.9 - '@zhead/schema-vue': 0.9.9 - transitivePeerDependencies: - - '@vue/composition-api' - dev: true - - /@vueuse/head/1.0.0-rc.14_vue@3.2.45: - resolution: {integrity: sha512-3DtOfSE1141IKPIq4AR5UXQZPWQFSd7E5f3M+HkBRyxWsyxbNBBmK5hqkSYc2ENoFXa3xPhLYZXJPKuxqfJmiA==} - peerDependencies: - vue: '>=2.7 || >=3' - dependencies: - '@vueuse/shared': 9.4.0_vue@3.2.45 - '@zhead/schema': 0.9.9 - '@zhead/schema-vue': 0.9.9_vue@3.2.45 + '@unhead/schema': 0.5.1 + '@unhead/ssr': 0.5.1 + '@unhead/vue': 0.5.1_vue@3.2.45 vue: 3.2.45 transitivePeerDependencies: - '@vue/composition-api' @@ -2644,17 +2679,8 @@ packages: - vue dev: true - /@vueuse/shared/9.4.0: - resolution: {integrity: sha512-fTuem51KwMCnqUKkI8B57qAIMcFovtGgsCtAeqxIzH3i6nE9VYge+gVfneNHAAy7lj8twbkNfqQSygOPJTm4tQ==} - dependencies: - vue-demi: 0.13.11 - transitivePeerDependencies: - - '@vue/composition-api' - - vue - dev: true - - /@vueuse/shared/9.4.0_vue@3.2.45: - resolution: {integrity: sha512-fTuem51KwMCnqUKkI8B57qAIMcFovtGgsCtAeqxIzH3i6nE9VYge+gVfneNHAAy7lj8twbkNfqQSygOPJTm4tQ==} + /@vueuse/shared/9.5.0_vue@3.2.45: + resolution: {integrity: sha512-HnnCWU1Vg9CVWRCcI8ohDKDRB2Sc4bTgT1XAIaoLSfVHHn+TKbrox6pd3klCSw4UDxkhDfOk8cAdcK+Z5KleCA==} dependencies: vue-demi: 0.13.11_vue@3.2.45 transitivePeerDependencies: @@ -2759,31 +2785,8 @@ packages: /@xtuc/long/4.2.2: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - /@zhead/schema-vue/0.9.9: - resolution: {integrity: sha512-f7sOPMc1zQJ+tDDWWaksNsGoGGuRv5aHvOdZvsL3dIxbiHVlGVhDi/HZbUUupCtlYAPv2D8E/tUmwWKh/UrbXw==} - peerDependencies: - vue: '>=2.7 || >=3' - dependencies: - '@vueuse/shared': 9.4.0 - '@zhead/schema': 0.9.9 - transitivePeerDependencies: - - '@vue/composition-api' - dev: true - - /@zhead/schema-vue/0.9.9_vue@3.2.45: - resolution: {integrity: sha512-f7sOPMc1zQJ+tDDWWaksNsGoGGuRv5aHvOdZvsL3dIxbiHVlGVhDi/HZbUUupCtlYAPv2D8E/tUmwWKh/UrbXw==} - peerDependencies: - vue: '>=2.7 || >=3' - dependencies: - '@vueuse/shared': 9.4.0_vue@3.2.45 - '@zhead/schema': 0.9.9 - vue: 3.2.45 - transitivePeerDependencies: - - '@vue/composition-api' - dev: false - - /@zhead/schema/0.9.9: - resolution: {integrity: sha512-B/No5zsZB1gz6BT7OKcD0rbyZCGoF6ImeQm2ffupQrgUpYAIv/LGtn3RVNSOcX2R2DB4g79UtuIwK0OxugFjJQ==} + /@zhead/schema/1.0.0-beta.13: + resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==} /abbrev/1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -8447,6 +8450,14 @@ packages: node-fetch-native: 0.1.7 pathe: 0.3.9 + /unhead/0.5.1: + resolution: {integrity: sha512-5ZZ0wNRWWdY8+YEg3sX4IXr5r2duc1JslUyfHX1rAGBsaJ62IJRxI6DmgZqSEN0yfqYclCZenxNG+rmWjPKFQw==} + dependencies: + '@unhead/dom': 0.5.1 + '@unhead/schema': 0.5.1 + hookable: 5.4.1 + dev: false + /unimport/0.7.0: resolution: {integrity: sha512-Cr0whz4toYVid3JHlni/uThwavDVVCk6Zw0Gxnol1c7DprTA+Isr4T+asO6rDGkhkgV7r3vSdSs5Ym8F15JA+w==} dependencies: diff --git a/test/basic.test.ts b/test/basic.test.ts index cb91e963803..b66407d5d2d 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -253,7 +253,7 @@ describe('head tags', () => { expect(headHtml).toMatch(/<html[^>]*class="html-attrs-test"/) expect(headHtml).toMatch(/<body[^>]*class="body-attrs-test"/) expect(headHtml).toContain('script>console.log("works with useMeta too")</script>') - expect(headHtml).toContain('<script src="https://a-body-appended-script.com" data-meta-body></script></body>') + expect(headHtml).toContain('<script src="https://a-body-appended-script.com"></script></body>') const indexHtml = await $fetch('/') // should render charset by default diff --git a/test/bundle.test.ts b/test/bundle.test.ts index fa515f9116e..7918bd23600 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -29,6 +29,7 @@ describe.skipIf(isWindows)('minimal nuxt application', () => { expect(stats.client.totalBytes).toBeLessThan(110000) expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(` [ + "_nuxt/composables.js", "_nuxt/entry.js", "_nuxt/error-404.js", "_nuxt/error-500.js", @@ -51,6 +52,9 @@ describe.skipIf(isWindows)('minimal nuxt application', () => { expect(packages).toMatchInlineSnapshot(` [ "@babel/parser", + "@unhead/dom", + "@unhead/ssr", + "@unhead/vue", "@vue/compiler-core", "@vue/compiler-dom", "@vue/compiler-ssr", @@ -78,6 +82,7 @@ describe.skipIf(isWindows)('minimal nuxt application', () => { "ufo", "unctx", "unenv", + "unhead", "unstorage", "vue", "vue-bundle-renderer", diff --git a/test/fixtures/basic/types.ts b/test/fixtures/basic/types.ts index c5cb34002b3..34e49bbd2e0 100644 --- a/test/fixtures/basic/types.ts +++ b/test/fixtures/basic/types.ts @@ -123,7 +123,6 @@ describe('runtimeConfig', () => { describe('head', () => { it('correctly types nuxt.config options', () => { - // @ts-expect-error defineNuxtConfig({ app: { head: { titleTemplate: () => 'test' } } }) defineNuxtConfig({ app: {