mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
feat(nuxt): migrate to latest @vueuse/head
(#8000)
This commit is contained in:
parent
a6e4a09dbb
commit
9e6d292ba6
@ -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>
|
||||
```
|
||||
|
||||
::
|
||||
|
@ -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"}
|
||||
|
@ -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"}
|
||||
::
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
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', () => {
|
||||
watchEffect(() => { head.updateDOM() })
|
||||
headReady = true
|
||||
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(() => {
|
||||
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)
|
||||
})
|
||||
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
|
||||
|
@ -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) {
|
||||
|
@ -22,6 +22,7 @@ export default defineBuildConfig({
|
||||
'vue-meta',
|
||||
'vue-router',
|
||||
'vue-bundle-renderer',
|
||||
'@vueuse/head',
|
||||
'vue',
|
||||
'hookable',
|
||||
'nitropack',
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
}
|
||||
|
@ -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
|
||||
|
3
test/fixtures/basic/pages/head.vue
vendored
3
test/fixtures/basic/pages/head.vue
vendored
@ -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>
|
||||
|
28
test/fixtures/basic/types.ts
vendored
28
test/fixtures/basic/types.ts
vendored
@ -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>>()
|
||||
|
42
yarn.lock
42
yarn.lock
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user