Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat(nuxt, schema): official @vueuse/head v1 support #8975

Merged
merged 21 commits into from Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 3 additions & 5 deletions docs/content/3.api/1.composables/use-head.md
Expand Up @@ -4,11 +4,9 @@ description: useHead customizes the head properties of individual pages of your

# `useHead`

Nuxt provides the `useHead` composable to add and customize the head properties of individual pages of your Nuxt app. It uses [@vueuse/head](https://github.com/vueuse/head) under the hood.
Nuxt provides the `useHead` composable to add and customize the head properties of individual pages of your Nuxt app.

::alert{icon=πŸ‘‰}
`useHead` only works during `setup` or `Lifecycle Hooks`.
::
`useHead` is powered by [@vueuse/head](https://github.com/vueuse/head), you can find more in-depth documentation [here](https://unhead.harlanzw.com/)

::ReadMore{link="/getting-started/seo-meta"}
::
Expand All @@ -19,7 +17,7 @@ Nuxt provides the `useHead` composable to add and customize the head properties
useHead(meta: MaybeComputedRef<MetaObject>): void
```

Below are the non-reactive types for `useMeta`. See [zhead](https://github.com/harlan-zw/zhead/tree/main/packages/schema/src) for more detailed types.
Below are the non-reactive types for `useHead`. See [zhead](https://github.com/harlan-zw/zhead/tree/main/packages/schema/src) for more detailed types.

```ts
interface MetaObject {
Expand Down
4 changes: 3 additions & 1 deletion packages/nuxt/package.json
Expand Up @@ -44,7 +44,9 @@
"@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.12",
"unhead": "^0.6.6",
"@unhead/ssr": "^0.6.6",
"chokidar": "^3.5.3",
"cookie-es": "^0.5.0",
"defu": "^6.1.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/nuxt/src/app/nuxt.ts
Expand Up @@ -6,6 +6,7 @@ import type { RuntimeConfig, AppConfigInput } from '@nuxt/schema'
import { getContext } from 'unctx'
import type { SSRContext } from 'vue-bundle-renderer/runtime'
import type { H3Event } from 'h3'
import type { HeadTag } from '@vueuse/head'

const nuxtAppCtx = getContext<NuxtApp>('nuxt-app')

Expand Down Expand Up @@ -37,6 +38,7 @@ export interface RuntimeNuxtHooks {
'page:transition:finish': (Component?: VNode) => HookResult
'vue:setup': () => void
'vue:error': (...args: Parameters<Parameters<typeof onErrorCaptured>[0]>) => HookResult
'head:tags:resolve': (ctx: { tags: HeadTag[] }) => HookResult
}

export interface NuxtSSRContext extends SSRContext {
Expand Down
8 changes: 8 additions & 0 deletions packages/nuxt/src/core/nitro.ts
Expand Up @@ -8,6 +8,8 @@ import defu from 'defu'
import fsExtra from 'fs-extra'
import { dynamicEventHandler } from 'h3'
import type { Plugin } from 'rollup'
import { createHeadCore } from 'unhead'
import { renderSSRHead } from '@unhead/ssr'
import { distDir } from '../dirs'
import { ImportProtectionPlugin } from './plugins/import-protection'

Expand Down Expand Up @@ -114,6 +116,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}
})

// Add head chunk for SPA renders
const head = createHeadCore()
head.push(nuxt.options.app.head)
const headChunk = await renderSSRHead(head)
nitroConfig.virtual!['#head-static'] = `export default ${JSON.stringify(headChunk)}`

// Add fallback server for `ssr: false`
if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'
Expand Down
5 changes: 4 additions & 1 deletion packages/nuxt/src/core/runtime/nitro/renderer.ts
Expand Up @@ -41,6 +41,9 @@ const getClientManifest: () => Promise<Manifest> = () => import('#build/dist/ser
.then(r => r.default || r)
.then(r => typeof r === 'function' ? r() : r) as Promise<ClientManifest>

// @ts-ignore
const getStaticRenderedHead = () : Promise<NuxtMeta> => import('#head-static').then(r => r.default || r)

// @ts-ignore
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)

Expand Down Expand Up @@ -102,7 +105,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
data: {},
state: {}
}
ssrContext!.renderMeta = ssrContext!.renderMeta ?? (() => ({}))
ssrContext!.renderMeta = ssrContext!.renderMeta ?? getStaticRenderedHead
return Promise.resolve(result)
}

Expand Down
11 changes: 11 additions & 0 deletions packages/nuxt/src/head/module.ts
Expand Up @@ -29,6 +29,17 @@ export default defineNuxtModule({
})
}

// add non useHead composables
nuxt.hooks.hook('imports:sources', (sources) => {
sources.push({
from: '@vueuse/head',
imports: [
'useSeoMeta',
'injectHead'
pi0 marked this conversation as resolved.
Show resolved Hide resolved
]
})
})

// Add mixin plugin
addPlugin({ src: resolve(runtimeDir, 'mixin-plugin') })

Expand Down
18 changes: 12 additions & 6 deletions 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 { HeadEntryOptions, UseHeadInput, ActiveHeadEntry } from '@vueuse/head'
import type { HeadAugmentations } from '@nuxt/schema'
import { useNuxtApp } from '#app'

/**
Expand All @@ -9,12 +9,18 @@ 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<MetaObject>) {
useNuxtApp()._useHead(meta)
export function useHead<T extends HeadAugmentations> (input: UseHeadInput<T>, options?: HeadEntryOptions): ActiveHeadEntry<UseHeadInput<T>> | void {
return useNuxtApp()._useHead(input, options)
}

export function useServerHead<T extends HeadAugmentations> (input: UseHeadInput<T>) {
if (process.server) {
return useHead(input, { mode: 'server' })
}
}

// TODO: remove useMeta support when Nuxt 3 is stable
/** @deprecated Please use new `useHead` composable instead */
export function useMeta (meta: MaybeComputedRef<MetaObject>) {
return useHead(meta)
export function useMeta<T extends HeadAugmentations> (input: UseHeadInput<T>) {
return useHead(input)
}
6 changes: 5 additions & 1 deletion packages/nuxt/src/head/runtime/index.ts
@@ -1,2 +1,6 @@
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<HeadAugmentations>
61 changes: 23 additions & 38 deletions packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts
@@ -1,63 +1,48 @@
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 })
head.push(appHead, {
// when SSR we don't need to hydrate this entry
mode: nuxtApp.ssrContext?.noSSR ? 'all' : 'server'
})

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()
}
})
// triggers dom update
head.internalHooks.callHook('entries:updated', head.unhead)
}
head.internalHooks.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 = (_meta: MaybeComputedRef<MetaObject>, options: HeadEntryOptions) => {
if (process.server) {
head.addEntry(_meta, options)
return
}

const cleanUp = head.addReactiveEntry(_meta, options)
// basic support for users to modify tags before render
head.internalHooks.hook('tags:resolve', ctx => nuxtApp.hooks.callHook('head:tags:resolve', ctx))
pi0 marked this conversation as resolved.
Show resolved Hide resolved

const vm = getCurrentInstance()
if (!vm) { return }

onBeforeUnmount(() => {
cleanUp()
head.updateDOM()
})
}
// useHead does not depend on a vue component context, we keep it on the nuxtApp for backwards compatibility
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.unhead)
return {
...meta,
bodyScriptsPrepend: meta.bodyTagsOpen,
// resolves naming difference with NuxtMeta and @vueuse/head
bodyScripts: meta.bodyTags
}
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/imports/presets.ts
Expand Up @@ -6,7 +6,8 @@ const commonPresets: InlinePreset[] = [
from: '#head',
imports: [
'useHead',
'useMeta'
'useMeta',
'useServerHead'
]
}),
// vue-demi (mocked)
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/build.config.ts
Expand Up @@ -22,7 +22,7 @@ export default defineBuildConfig({
'vue-meta',
'vue-router',
'vue-bundle-renderer',
'@vueuse/head',
'@unhead/schema',
'vue',
'hookable',
'nitropack',
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/package.json
Expand Up @@ -17,7 +17,7 @@
"@types/lodash.template": "^4",
"@types/semver": "^7",
"@vitejs/plugin-vue": "^3.2.0",
"@vueuse/head": "~1.0.0-rc.14",
"@unhead/schema": "^0.6.6",
"nitropack": "^0.6.1",
"unbuild": "latest",
"vite": "~3.2.3"
Expand Down
74 changes: 4 additions & 70 deletions packages/schema/src/types/meta.ts
@@ -1,6 +1,6 @@
import type { HeadObjectPlain, HeadObject } from '@vueuse/head'
import type { Head, MergeHead } from '@unhead/schema'

export interface HeadAugmentations {
export interface HeadAugmentations extends MergeHead {
// runtime type modifications
base?: {}
link?: {}
Expand All @@ -12,7 +12,8 @@ export interface HeadAugmentations {
bodyAttrs?: {}
}

export type MetaObjectRaw = HeadObjectPlain<HeadAugmentations>
export type MetaObjectRaw = Head<HeadAugmentations>
export type MetaObject = MetaObjectRaw

export type AppHeadMetaObject = MetaObjectRaw & {
/**
Expand All @@ -29,70 +30,3 @@ export type AppHeadMetaObject = MetaObjectRaw & {
*/
viewport?: string
}

export interface MetaObject {
/**
* The <title> 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']
}