feat(nuxt): migrate to latest `@vueuse/head` (#8000)

This commit is contained in:
Harlan Wilton 2022-10-13 04:00:17 +11:00 committed by GitHub
parent a6e4a09dbb
commit 9e6d292ba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 470 additions and 280 deletions

View File

@ -1,34 +1,68 @@
---
navigation.icon: uil:file-search-alt
description: Nuxt provides good default values for meta tags, but you can override these if you need to.
description: Improve your Nuxt app's SEO with powerful head config, composables and components.
---
# SEO and Meta
Out-of-the-box, Nuxt provides good default values for `charset` and `viewport` meta tags, but you can override these if you need to, as well as customize other meta tags for your site in several different ways.
Improve your Nuxt app's SEO with powerful head config, composables and components.
:ReadMore{link="/api/configuration/nuxt-config#head"}
## App Head
## `useHead` Composable
Providing an [app.head](/api/configuration/nuxt-config#head) property in your `nuxt.config.ts` allows you to customize the head for your entire app.
Within your `setup` function, you can call `useHead` with an object of meta properties with keys corresponding to meta tags: `title`, `titleTemplate`, `base`, `script`, `noscript`, `style`, `meta` and `link`, as well as `htmlAttrs` and `bodyAttrs`. There are also two shorthand properties, `charset` and `viewport`, which set the corresponding meta tags. Alternatively, you can pass a function returning the object for reactive metadata.
::alert{type=info}
This method does not allow you to provide reactive data, if you need global reactive data you can use `useHead` in `app.vue`.
::
For example:
Shortcuts are available to make configuration easier: `charset` and `viewport`. You can also provide any other key listed below in [Types](#types).
```vue
<script setup>
### Defaults
Out-of-the-box, Nuxt provides sane defaults, which you can override if needed.
- `charset`: `utf-8`
- `viewport`: `width=device-width, initial-scale=1`
### Example
```ts{}[nuxt.config.ts]
export default defineNuxtConfig({
app: {
head: {
charset: 'utf-16',
viewport: 'width=500, initial-scale=1',
title: 'My App',
meta: [
// <meta name="description" content="My amazing site">
{ name: 'description', content: 'My amazing site.' }
],
}
}
})
```
:ReadMore{link="/api/configuration/nuxt-config/#head"}
## Composable: `useHead`
The `useHead` composable function allows you to manage your head tags in a programmatic and reactive way, powered by [@vueuse/head](https://github.com/vueuse/head).
As with all composables, it can only be used with a components `setup` and lifecycle hooks.
### Example
```vue{}[app.vue]
<script setup lang="ts">
useHead({
title: 'My App',
// or, instead:
// titleTemplate: (title) => `My App - ${title}`,
viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
charset: 'utf-8',
meta: [
{ name: 'description', content: 'My amazing site.' }
],
bodyAttrs: {
class: 'test'
}
},
script: [ { children: 'console.log(\'Hello world\') } ]
})
</script>
```
@ -36,44 +70,7 @@ useHead({
::ReadMore{link="/api/composables/use-head"}
::
## Title Templates
You can use the `titleTemplate` option to provide a dynamic template for customizing the title of your site, for example, by adding the name of your site to the title of every page.
The `titleTemplate` can either be a string, where `%s` is replaced with the title, or a function. If you want to use a function (for full control), then this cannot be set in your `nuxt.config`, and it is recommended instead to set it within your `app.vue` file, where it will apply to all pages on your site:
```vue [app.vue]
<script setup>
useHead({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
}
})
</script>
```
Now, if you set the title to `My Page` with `useHead` on another page of your site, the title would appear as 'My Page - Site Title' in the browser tab. You could also pass `null` to default to the site title.
## Body Meta Tags
You can use the `body: true` option on the `link` and `script` meta tags to append them to the end of the `<body>` tag.
For example:
```vue
<script setup>
useHead({
script: [
{
src: 'https://third-party-script.com',
body: true
}
]
})
</script>
```
## Meta Components
## Components
Nuxt provides `<Title>`, `<Base>`, `<Script>`, `<NoScript>`, `<Style>`, `<Meta>`, `<Link>`, `<Body>`, `<Html>` and `<Head>` components so that you can interact directly with your metadata within your component's template.
@ -81,7 +78,7 @@ Because these component names match native HTML elements, it is very important t
`<Head>` and `<Body>` can accept nested meta tags (for aesthetic reasons) but this has no effect on _where_ the nested meta tags are rendered in the final HTML.
For example:
### Example
<!-- @case-police-ignore html -->
@ -103,7 +100,120 @@ const title = ref('Hello World')
</template>
```
## Example: Usage With `definePageMeta`
## Types
The below is the non-reactive types used for `useHead`, `app.head` and components.
```ts
interface MetaObject {
title?: string
titleTemplate?: string | ((title?: string) => string)
base?: Base
link?: Link[]
meta?: Meta[]
style?: Style[]
script?: Script[]
noscript?: Noscript[];
htmlAttrs?: HtmlAttributes;
bodyAttrs?: BodyAttributes;
}
```
See [zhead](https://github.com/harlan-zw/zhead/tree/main/packages/schema/src) for more detailed types.
## Features
### Reactivity
Reactivity is supported on all properties, as computed, computed getter refs and reactive.
It's recommended to use computed getters (`() => {}`) over computed (`computed(() => {})`).
::code-group
```vue [useHead]
<script setup lang="ts">
const desc = ref('My amazing site.')
useHead({
meta: [
{ name: 'description', content: desc }
],
})
</script>
```
```vue [Components]
<script setup>
const desc = ref('My amazing site.')
</script>
<template>
<div>
<Meta name="description" :content="desc" />
</div>
</template>
```
::
### Title Templates
You can use the `titleTemplate` option to provide a dynamic template for customizing the title of your site. for example, by adding the name of your site to the title of every page.
The `titleTemplate` can either be a string, where `%s` is replaced with the title, or a function.
If you want to use a function (for full control), then this cannot be set in your `nuxt.config`, and it is recommended instead to set it within your `app.vue` file, where it will apply to all pages on your site:
::code-group
```vue [useHead]
<script setup lang="ts">
useHead({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title';
}
})
</script>
```
::
Now, if you set the title to `My Page` with `useHead` on another page of your site, the title would appear as 'My Page - Site Title' in the browser tab. You could also pass `null` to default to the site title.
### Body Tags
You can use the `body: true` option on the `link` and `script` meta tags to append them to the end of the `<body>` tag.
For example:
::code-group
```vue [useHead]
<script setup lang="ts">
useHead({
script: [
{
src: 'https://third-party-script.com',
body: true
}
]
})
</script>
```
```vue [Components]
<template>
<div>
<Script src="https://third-party-script.com" body="true" />
</div>
</template>
```
::
## Examples
### Usage With `definePageMeta`
Within your `pages/` directory, you can use `definePageMeta` along with `useHead` to set metadata based on the current route.
@ -132,3 +242,60 @@ useHead({
:LinkExample{link="/examples/composables/use-head"}
:ReadMore{link="/guide/directory-structure/pages/#page-metadata"}
### Add Dynamic Title
In the example below, `titleTemplate` is set either as a string with the `%s` placeholder or as a `function`, which allows greater flexibility in setting the page title dynamically for each route of your Nuxt app:
```vue [app.vue]
<script setup>
useHead({
// as a string,
// where `%s` is replaced with the title
titleTemplate: '%s - Site Title',
// ... or as a function
titleTemplate: (productCategory) => {
return productCategory
? `${productCategory} - Site Title`
: 'Site Title'
}
})
</script>
```
`nuxt.config` is also used as an alternative way of setting the page title. However, `nuxt.config` does not allow the page title to be dynamic. Therefore, it is recommended to use `titleTemplate` in the `app.vue` file to add a dynamic title, which is then applied to all routes of your Nuxt app.
### Add External CSS
The example below inserts Google Fonts using the `link` property of the `useHead` composable:
::code-group
```vue [useHead]
<script setup lang="ts">
useHead({
link: [
{
rel: 'preconnect',
href: 'https://fonts.googleapis.com'
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap',
crossorigin: ''
}
]
})
</script>
```
```vue [Components]
<template>
<div>
<Link rel="preconnect" href="https://fonts.googleapis.com" />
<Link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" crossorigin="" />
</div>
</template>
```
::

View File

@ -1,5 +1,5 @@
---
description: useHead customizes the head properties of individual pages of your Nuxt app.
description: useHead customizes the head properties of individual pages of your Nuxt app.
---
# `useHead`
@ -10,27 +10,31 @@ Nuxt provides the `useHead` composable to add and customize the head properties
`useHead` only works during `setup` or `Lifecycle Hooks`.
::
::ReadMore{link="/getting-started/seo-meta"}
::
## Type
```ts
useHead(meta: Computable<MetaObject>): void
interface MetaObject extends Record<string, any> {
charset?: string
viewport?: string
meta?: Array<Record<string, any>>
link?: Array<Record<string, any>>
style?: Array<Record<string, any>>
script?: Array<Record<string, any>>
noscript?: Array<Record<string, any>>
titleTemplate?: string | ((title: string) => string)
title?: string
bodyAttrs?: Record<string, any>
htmlAttrs?: Record<string, any>
}
useHead(meta: MaybeComputedRef<MetaObject>): void
```
Application-wide configuration of the head metadata is possible through [nuxt.config](/api/configuration/nuxt-config#head), or by placing the `useHead` in the `app.vue` file.
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.
```ts
interface MetaObject {
title?: string
titleTemplate?: string | ((title?: string) => string)
base?: Base
link?: Link[]
meta?: Meta[]
style?: Style[]
script?: Script[]
noscript?: Noscript[]
htmlAttrs?: HtmlAttributes
bodyAttrs?: BodyAttributes
}
```
::alert{type=info}
The properties of `useHead` can be dynamic, accepting `ref`, `computed` and `reactive` properties. `meta` parameter can also accept a function returning an object to make the entire object reactive.
@ -44,28 +48,10 @@ The properties of `useHead` can be dynamic, accepting `ref`, `computed` and `rea
An object accepting the following head metadata:
- `charset`
**Type**: `string`
**Default**: `utf-8`
Specifies character encoding for the HTML document.
- `viewport`
**Type**: `string`
**Default**: `width=device-width, initial-scale=1`
Configures the viewport (the user's visible area of a web page).
- `meta`
**Type**: `Array<Record<string, any>>`
**Default**: `width=device-width, initial-scale=1`
Each element in the array is mapped to a newly-created `<meta>` tag, where object properties are mapped to the corresponding attributes.
- `link`
@ -115,91 +101,3 @@ An object accepting the following head metadata:
**Type**: `Record<string, any>`
Sets attributes of the `<html>` tag. Each object property is mapped to the corresponding attribute.
## Examples
### Customize Metadata
The example below changes the website's `title` and `description` using `meta` option of the `useHead` composable:
```vue
<script setup>
const title = ref('My App')
const description = ref('My amazing Nuxt app')
useHead({
title,
meta: [
{
name: 'description',
content: description
}
]
})
</script>
```
### Add Dynamic Title
In the example below, `titleTemplate` is set either as a string with the `%s` placeholder or as a `function`, which allows greater flexibility in setting the page title dynamically for each route of your Nuxt app:
```vue [app.vue]
<script setup>
useHead({
// as a string,
// where `%s` is replaced with the title
titleTemplate: '%s - Site Title',
// ... or as a function
titleTemplate: (productCategory) => {
return productCategory
? `${productCategory} - Site Title`
: 'Site Title'
}
})
</script>
```
`nuxt.config` is also used as an alternative way of setting the page title. However, `nuxt.config` does not allow the page title to be dynamic. Therefore, it is recommended to use `titleTemplate` in the `app.vue` file to add a dynamic title, which is then applied to all routes of your Nuxt app.
### Add External CSS
The example below inserts Google Fonts using the `link` property of the `useHead` composable:
```vue
<script setup>
useHead({
link: [
{
rel: 'preconnect',
href: 'https://fonts.googleapis.com'
},
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap',
crossorigin: ''
}
]
})
</script>
```
### Add Third-party Script
The example below inserts a third-party script using the `script` property of the `useHead` composable:
```vue
<script setup>
useHead({
script: [
{
src: 'https://third-party-script.com',
body: true
}
]
})
</script>
```
You can use the `body: true` option to add the above script at the end of the `<body>` tag.
:ReadMore{link="/guide/features/head-management"}

View File

@ -4,10 +4,6 @@ description: "This example shows how to use useHead and Nuxt built-in components
title: "useHead"
---
::alert{type=info icon=👉}
Learn more about [meta tags](/guide/features/head-management#meta-components).
::
::ReadMore{link="/api/composables/use-head"}
::

View File

@ -44,7 +44,7 @@
"@nuxt/vite-builder": "3.0.0-rc.11",
"@vue/reactivity": "^3.2.40",
"@vue/shared": "^3.2.40",
"@vueuse/head": "^0.7.12",
"@vueuse/head": "~1.0.0-rc.7",
"chokidar": "^3.5.3",
"cookie-es": "^0.5.0",
"defu": "^6.1.0",

View File

@ -1,13 +1,13 @@
import { defineComponent, PropType } from 'vue'
import type { SetupContext } from 'vue'
import { defineComponent } from 'vue'
import type { PropType, SetupContext } from 'vue'
import { useHead } from './composables'
import type {
Props,
FetchPriority,
CrossOrigin,
FetchPriority,
HTTPEquiv,
ReferrerPolicy,
LinkRelationship,
Props,
ReferrerPolicy,
Target
} from './types'
@ -88,7 +88,9 @@ export const Script = defineComponent({
/** @deprecated **/
charset: String,
/** @deprecated **/
language: String
language: String,
body: Boolean,
renderPriority: [String, Number]
},
setup: setupForUseMeta((props, { slots }) => {
const script = { ...props }
@ -111,7 +113,9 @@ export const NoScript = defineComponent({
inheritAttrs: false,
props: {
...globalProps,
title: String
title: String,
body: Boolean,
renderPriority: [String, Number]
},
setup: setupForUseMeta((props, { slots }) => {
const noscript = { ...props }
@ -157,7 +161,9 @@ export const Link = defineComponent({
/** @deprecated **/
methods: String,
/** @deprecated **/
target: String as PropType<Target>
target: String as PropType<Target>,
body: Boolean,
renderPriority: [String, Number]
},
setup: setupForUseMeta(link => ({
link: [link]
@ -205,7 +211,9 @@ export const Meta = defineComponent({
charset: String,
content: String,
httpEquiv: String as PropType<HTTPEquiv>,
name: String
name: String,
body: Boolean,
renderPriority: [String, Number]
},
setup: setupForUseMeta((props) => {
const meta = { ...props }
@ -235,7 +243,9 @@ export const Style = defineComponent({
scoped: {
type: Boolean,
default: undefined
}
},
body: Boolean,
renderPriority: [String, Number]
},
setup: setupForUseMeta((props, { slots }) => {
const style = { ...props }
@ -269,7 +279,8 @@ export const Html = defineComponent({
...globalProps,
manifest: String,
version: String,
xmlns: String
xmlns: String,
renderPriority: [String, Number]
},
setup: setupForUseMeta(htmlAttrs => ({ htmlAttrs }), true)
})
@ -279,6 +290,9 @@ export const Body = defineComponent({
// eslint-disable-next-line vue/no-reserved-component-names
name: 'Body',
inheritAttrs: false,
props: globalProps,
props: {
...globalProps,
renderPriority: [String, Number]
},
setup: setupForUseMeta(bodyAttrs => ({ bodyAttrs }), true)
})

View File

@ -1,11 +1,7 @@
import { isFunction } from '@vue/shared'
import { computed } from 'vue'
import type { ComputedGetter, ComputedRef } from '@vue/reactivity'
import type { MetaObject } from '@nuxt/schema'
import type { MaybeComputedRef } from '@vueuse/head'
import { useNuxtApp } from '#app'
type Computable<T> = T extends Record<string, any> ? ComputedGetter<T> | { [K in keyof T]: T[K] | ComputedRef<T[K]> } : T
/**
* You can pass in a meta object, which has keys corresponding to meta tags:
* `title`, `base`, `script`, `style`, `meta` and `link`, as well as `htmlAttrs` and `bodyAttrs`.
@ -13,13 +9,12 @@ type Computable<T> = T extends Record<string, any> ? ComputedGetter<T> | { [K in
* Alternatively, for reactive meta state, you can pass in a function
* that returns a meta object.
*/
export function useHead (meta: Computable<MetaObject>) {
const resolvedMeta = isFunction(meta) ? computed(meta) : meta
useNuxtApp()._useHead(resolvedMeta)
export function useHead (meta: MaybeComputedRef<MetaObject>) {
useNuxtApp()._useHead(meta)
}
// TODO: remove useMeta support when Nuxt 3 is stable
/** @deprecated Please use new `useHead` composable instead */
export function useMeta (meta: Computable<MetaObject>) {
export function useMeta (meta: MaybeComputedRef<MetaObject>) {
return useHead(meta)
}

View File

@ -2,10 +2,13 @@ import { createApp } from 'vue'
import { createMetaManager } from 'vue-meta'
import type { MetaObject } from '..'
import { defineNuxtPlugin } from '#app'
// @ts-expect-error untyped
import { appHead } from '#build/nuxt.config.mjs'
export default defineNuxtPlugin((nuxtApp) => {
// @ts-expect-error missing resolver
const manager = createMetaManager(process.server)
manager.addMeta(appHead)
nuxtApp.vueApp.use(manager)

View File

@ -1,52 +1,58 @@
import type { HeadEntryOptions, MaybeComputedRef } from '@vueuse/head'
import { createHead, renderHeadToString } from '@vueuse/head'
import { computed, ref, watchEffect, onBeforeUnmount, getCurrentInstance, ComputedGetter } from 'vue'
import defu from 'defu'
import type { MetaObject } from '..'
import { defineNuxtPlugin } from '#app'
import { onBeforeUnmount, getCurrentInstance } from 'vue'
import type { MetaObject } from '@nuxt/schema'
import { defineNuxtPlugin, useRouter } 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 })
nuxtApp.vueApp.use(head)
let headReady = false
nuxtApp.hooks.hookOnce('app:mounted', () => {
watchEffect(() => { head.updateDOM() })
headReady = true
})
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', () => {
pauseDOMUpdates = false
head.updateDOM()
nuxtApp._useHead = (_meta: MetaObject | ComputedGetter<MetaObject>) => {
const meta = ref<MetaObject>(_meta)
const headObj = computed(() => {
const overrides: MetaObject = { meta: [] }
if (meta.value.charset) {
overrides.meta!.push({ key: 'charset', charset: meta.value.charset })
}
if (meta.value.viewport) {
overrides.meta!.push({ name: 'viewport', content: meta.value.viewport })
}
return defu(overrides, meta.value)
// 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(() => {
pauseDOMUpdates = false
head.updateDOM()
})
})
head.addHeadObjs(headObj as any)
}
if (process.server) { return }
if (headReady) {
watchEffect(() => { head.updateDOM() })
nuxtApp._useHead = (_meta: MaybeComputedRef<MetaObject>, options: HeadEntryOptions) => {
if (process.server) {
head.addEntry(_meta, options)
return
}
const cleanUp = head.addReactiveEntry(_meta, options)
const vm = getCurrentInstance()
if (!vm) { return }
onBeforeUnmount(() => {
head.removeHeadObjs(headObj as any)
cleanUp()
head.updateDOM()
})
}
if (process.server) {
nuxtApp.ssrContext!.renderMeta = () => {
const meta = renderHeadToString(head)
nuxtApp.ssrContext!.renderMeta = async () => {
const meta = await renderHeadToString(head)
return {
...meta,
// resolves naming difference with NuxtMeta and @vueuse/head

View File

@ -1,9 +1,7 @@
import { computed, getCurrentInstance, markRaw } from 'vue'
import { getCurrentInstance } from 'vue'
import * as Components from './components'
import { useHead } from './composables'
import { defineNuxtPlugin, useNuxtApp } from '#app'
// @ts-ignore
import { appHead } from '#build/nuxt.config.mjs'
type MetaComponents = typeof Components
declare module '@vue/runtime-core' {
@ -20,7 +18,7 @@ const metaMixin = {
const nuxtApp = useNuxtApp()
const source = typeof options.head === 'function'
? computed(() => options.head(nuxtApp))
? () => options.head(nuxtApp)
: options.head
useHead(source)
@ -28,8 +26,6 @@ const metaMixin = {
}
export default defineNuxtPlugin((nuxtApp) => {
useHead(markRaw({ title: '', ...appHead }))
nuxtApp.vueApp.mixin(metaMixin)
for (const name in Components) {

View File

@ -22,6 +22,7 @@ export default defineBuildConfig({
'vue-meta',
'vue-router',
'vue-bundle-renderer',
'@vueuse/head',
'vue',
'hookable',
'nitropack',

View File

@ -17,6 +17,7 @@
"@types/lodash.template": "^4",
"@types/semver": "^7",
"@vitejs/plugin-vue": "^3.1.2",
"@vueuse/head": "~1.0.0-rc.7",
"unbuild": "latest",
"vite": "~3.1.7"
},

View File

@ -2,8 +2,7 @@ import { resolve, join } from 'pathe'
import { existsSync, readdirSync } from 'node:fs'
import defu from 'defu'
import { defineUntypedSchema } from 'untyped'
import { MetaObject } from '../types/meta'
import type { AppHeadMetaObject } from '../types/meta'
export default defineUntypedSchema({
/**
@ -116,7 +115,7 @@ export default defineUntypedSchema({
*/
head: {
$resolve: async (val, get) => {
const resolved: Required<MetaObject> = defu(val, await get('meta'), {
const resolved: Required<AppHeadMetaObject> = defu(val, await get('meta'), {
meta: [],
link: [],
style: [],
@ -124,9 +123,15 @@ export default defineUntypedSchema({
noscript: []
})
resolved.charset = resolved.charset ?? resolved.meta.find(m => m.charset)?.charset ?? 'utf-8'
resolved.viewport = resolved.viewport ?? resolved.meta.find(m => m.name === 'viewport')?.content ?? 'width=device-width, initial-scale=1'
resolved.meta = resolved.meta.filter(m => m && m.name !== 'viewport' && !m.charset)
// provides default charset and viewport if not set
if (!resolved.meta.find(m => m.charset)?.charset) {
resolved.meta.unshift({ charset: resolved.charset || 'utf-8' })
}
if (!resolved.meta.find(m => m.name === 'viewport')?.content) {
resolved.meta.unshift({ name: 'viewport', content: resolved.viewport || 'width=device-width, initial-scale=1' })
}
resolved.meta = resolved.meta.filter(Boolean)
resolved.link = resolved.link.filter(Boolean)
resolved.style = resolved.style.filter(Boolean)
resolved.script = resolved.script.filter(Boolean)
@ -237,7 +242,7 @@ export default defineUntypedSchema({
},
/**
* @type {typeof import('../src/types/meta').MetaObject}
* @type {typeof import('../src/types/meta').AppHeadMetaObject}
* @version 3
* @deprecated - use `head` instead
*/

View File

@ -2,7 +2,7 @@ import type { KeepAliveProps, TransitionProps } from 'vue'
import { ConfigSchema } from '../../schema/config'
import type { ServerOptions as ViteServerOptions, UserConfig as ViteUserConfig } from 'vite'
import type { Options as VuePluginOptions } from '@vitejs/plugin-vue'
import type { MetaObject } from './meta'
import type { AppHeadMetaObject } from './meta'
import type { Nuxt } from './nuxt'
type DeepPartial<T> = T extends Function ? T : T extends Record<string, any> ? { [P in keyof T]?: DeepPartial<T[P]> } : T
@ -82,7 +82,7 @@ export interface AppConfigInput extends Record<string, any> {
}
export interface NuxtAppConfig {
head: MetaObject
head: AppHeadMetaObject
layoutTransition: boolean | TransitionProps
pageTransition: boolean | TransitionProps
keepalive: boolean | KeepAliveProps

View File

@ -1,4 +1,20 @@
export interface MetaObject extends Record<string, any> {
import type { HeadObjectPlain, HeadObject } from '@vueuse/head'
export interface HeadAugmentations {
// runtime type modifications
base?: {}
link?: {}
meta?: {}
style?: {}
script?: {}
noscript?: {}
htmlAttrs?: {}
bodyAttrs?: {}
}
export type MetaObjectRaw = HeadObjectPlain<HeadAugmentations>
export type AppHeadMetaObject = MetaObjectRaw & {
/**
* The character encoding in which the document is encoded => `<meta charset="<value>" />`
*
@ -12,21 +28,71 @@ export interface MetaObject extends Record<string, any> {
* @default `'width=device-width, initial-scale=1'`
*/
viewport?: string
/** Each item in the array maps to a newly-created `<meta>` element, where object properties map to attributes. */
meta?: Array<Record<string, any>>
/** Each item in the array maps to a newly-created `<link>` element, where object properties map to attributes. */
link?: Array<Record<string, any>>
/** Each item in the array maps to a newly-created `<style>` element, where object properties map to attributes. */
style?: Array<Record<string, any>>
/** Each item in the array maps to a newly-created `<script>` element, where object properties map to attributes. */
script?: Array<Record<string, any>>
/** Each item in the array maps to a newly-created `<noscript>` element, where object properties map to attributes. */
noscript?: Array<Record<string, any>>
titleTemplate?: string | ((title: string) => string)
title?: string
bodyAttrs?: Record<string, any>
htmlAttrs?: Record<string, any>
}
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']
}

View File

@ -241,6 +241,7 @@ describe('pages', () => {
describe('head tags', () => {
it('should render tags', async () => {
const headHtml = await $fetch('/head')
expect(headHtml).toContain('<title>Using a dynamic component - Title Template Fn Change</title>')
expect(headHtml).not.toContain('<meta name="description" content="first">')
expect(headHtml).toContain('<meta charset="utf-16">')
@ -252,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="true"></script></body>')
expect(headHtml).toContain('<script src="https://a-body-appended-script.com" data-meta-body></script></body>')
const indexHtml = await $fetch('/')
// should render charset by default

View File

@ -1,5 +1,6 @@
<script setup>
const a = ref('')
useHead({
// title template function example
titleTemplate: title => `${title} - Title Template Fn Change`,
@ -14,7 +15,7 @@ useHead({
],
meta: [{ name: 'description', content: 'first' }]
})
useHead({ charset: 'utf-16', meta: [{ name: 'description', content: computed(() => `${a.value} with an inline useHead call`) }] })
useHead({ meta: [{ charset: 'utf-16' }, { name: 'description', content: computed(() => `${a.value} with an inline useHead call`) }] })
useMeta({ script: [{ children: 'console.log("works with useMeta too")' }] })
a.value = 'overriding'
</script>

View File

@ -120,6 +120,34 @@ describe('runtimeConfig', () => {
})
})
describe('head', () => {
it('correctly types nuxt.config options', () => {
// @ts-expect-error
defineNuxtConfig({ app: { head: { titleTemplate: () => 'test' } } })
defineNuxtConfig({
app: {
head: {
meta: [{ key: 'key', name: 'description', content: 'some description ' }],
titleTemplate: 'test %s'
}
}
})
})
it('types useHead', () => {
useHead({
base: { href: '/base' },
link: computed(() => []),
meta: [
{ key: 'key', name: 'description', content: 'some description ' },
() => ({ key: 'key', name: 'description', content: 'some description ' })
],
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} - Site Title` : 'Site Title'
}
})
})
})
describe('composables', () => {
it('allows providing default refs', () => {
expectTypeOf(useState('test', () => ref('hello'))).toEqualTypeOf<Ref<string>>()

View File

@ -1835,6 +1835,7 @@ __metadata:
"@types/lodash.template": ^4
"@types/semver": ^7
"@vitejs/plugin-vue": ^3.1.2
"@vueuse/head": ~1.0.0-rc.7
c12: ^0.2.13
create-require: ^1.1.1
defu: ^6.1.0
@ -3460,14 +3461,16 @@ __metadata:
languageName: node
linkType: hard
"@vueuse/head@npm:^0.7.12":
version: 0.7.13
resolution: "@vueuse/head@npm:0.7.13"
"@vueuse/head@npm:~1.0.0-rc.7":
version: 1.0.0-rc.7
resolution: "@vueuse/head@npm:1.0.0-rc.7"
dependencies:
"@zhead/schema-vue": ^0.7.3
"@vueuse/shared": ^9.3.0
"@zhead/schema": ^0.9.5
"@zhead/schema-vue": ^0.9.5
peerDependencies:
vue: ">=2.7 || >=3"
checksum: 90f755536a83ebcc292b43abcb3e18ae03db127916c75383c58ba06c0829bb979aee26cf162fa9b9e8a58a6fcf1f01966fefde4fc2d4ba991949f000a24dc86b
checksum: cfb3b0edc92b97a93e0cd0af6ea082b1c44b41462fb231996be821f698396234804586baa29ad056add4de1e115bb4fb031ac62436f59182b2f3eaae432d70ea
languageName: node
linkType: hard
@ -3545,6 +3548,15 @@ __metadata:
languageName: node
linkType: hard
"@vueuse/shared@npm:^9.3.0":
version: 9.3.0
resolution: "@vueuse/shared@npm:9.3.0"
dependencies:
vue-demi: "*"
checksum: c20fcfbbad3a17fa26191823f4022b7dd6f7a6e5ede648466562f3b9f4268fb417cd825ed002e2d74ef8f81971a3ca1691f35b4497676173b62c077b2a17d032
languageName: node
linkType: hard
"@webassemblyjs/ast@npm:1.11.1":
version: 1.11.1
resolution: "@webassemblyjs/ast@npm:1.11.1"
@ -3727,22 +3739,22 @@ __metadata:
languageName: node
linkType: hard
"@zhead/schema-vue@npm:^0.7.3":
version: 0.7.4
resolution: "@zhead/schema-vue@npm:0.7.4"
"@zhead/schema-vue@npm:^0.9.5":
version: 0.9.5
resolution: "@zhead/schema-vue@npm:0.9.5"
dependencies:
"@vueuse/shared": ^9.2.0
"@zhead/schema": 0.7.4
"@zhead/schema": 0.9.5
peerDependencies:
vue: ">=2.7 || >=3"
checksum: 96487c101c7587ad7fa2c9e3f089cc38eb349e9e8464c243e9c06742ed699e6ca7fc8eb966a06440fc7ad544ae232503569de692ec4d94a8fee64ef5a47b6d1d
checksum: dda369075fa47cbfed41cdb414a39002b61231ed7d2098547edd1cf70b287523b10fdbc7351acc338c31d2885e2b8ab5b6c8fd1f4b70b9a591ac457afafe6a3b
languageName: node
linkType: hard
"@zhead/schema@npm:0.7.4":
version: 0.7.4
resolution: "@zhead/schema@npm:0.7.4"
checksum: a2e87971582b30fed4f5d69ef885a965cc2eb38d418cf78b6db4fb1d718c92148970375286d9f50115467d627ef73dbdb8effb8d5ee37065871eaa2060d4ceec
"@zhead/schema@npm:0.9.5, @zhead/schema@npm:^0.9.5":
version: 0.9.5
resolution: "@zhead/schema@npm:0.9.5"
checksum: 88577289337b5f7b3e38d80c004f0733cdb44d0be2fc73bf13de8325dca701d5eeec53fffa176a19f56ad7d8c04c81c3e734bb650752bef91a0d72d0a61e8f5d
languageName: node
linkType: hard
@ -10920,7 +10932,7 @@ __metadata:
"@types/hash-sum": ^1.0.0
"@vue/reactivity": ^3.2.40
"@vue/shared": ^3.2.40
"@vueuse/head": ^0.7.12
"@vueuse/head": ~1.0.0-rc.7
chokidar: ^3.5.3
cookie-es: ^0.5.0
defu: ^6.1.0