diff --git a/docs/content/1.getting-started/5.seo-meta.md b/docs/content/1.getting-started/5.seo-meta.md index 6b4ab49e328..636a5b56feb 100644 --- a/docs/content/1.getting-started/5.seo-meta.md +++ b/docs/content/1.getting-started/5.seo-meta.md @@ -1,79 +1,76 @@ --- 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. - -For example: +::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`. +:: -```vue - -``` +Shortcuts are available to make configuration easier: `charset` and `viewport`. You can also provide any other key listed below in [Types](#types). -::ReadMore{link="/api/composables/use-head"} -:: +### Defaults -## Title Templates +Out-of-the-box, Nuxt provides sane defaults, which you can override if needed. -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. +- `charset`: `utf-8` +- `viewport`: `width=device-width, initial-scale=1` -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: +### Example -```vue [app.vue] - + } +}) ``` -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. +:ReadMore{link="/api/configuration/nuxt-config/#head"} -## Body Meta Tags +## Composable: `useHead` -You can use the `body: true` option on the `link` and `script` meta tags to append them to the end of the `` tag. +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). -For example: +As with all composables, it can only be used with a components `setup` and lifecycle hooks. -```vue - ``` -## Meta Components +::ReadMore{link="/api/composables/use-head"} +:: + +## Components Nuxt provides ``, `<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> + ``` + +:: diff --git a/docs/content/3.api/1.composables/use-head.md b/docs/content/3.api/1.composables/use-head.md index f34b26f0f9f..8fb170a0df2 100644 --- a/docs/content/3.api/1.composables/use-head.md +++ b/docs/content/3.api/1.composables/use-head.md @@ -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,28 +10,32 @@ 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) +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. + +```ts +interface MetaObject { title?: string - bodyAttrs?: Record<string, any> - htmlAttrs?: Record<string, any> + titleTemplate?: string | ((title?: string) => string) + base?: Base + link?: Link[] + meta?: Meta[] + style?: Style[] + script?: Script[] + noscript?: Noscript[] + htmlAttrs?: HtmlAttributes + bodyAttrs?: BodyAttributes } ``` -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. - ::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"} diff --git a/docs/content/4.examples/3.composables/use-head.md b/docs/content/4.examples/3.composables/use-head.md index 529eabc4cd3..6251ed68ca9 100644 --- a/docs/content/4.examples/3.composables/use-head.md +++ b/docs/content/4.examples/3.composables/use-head.md @@ -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"} :: diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index c393a32b54d..e1c3816f986 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -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", diff --git a/packages/nuxt/src/head/runtime/components.ts b/packages/nuxt/src/head/runtime/components.ts index db72e444ce8..637660b4745 100644 --- a/packages/nuxt/src/head/runtime/components.ts +++ b/packages/nuxt/src/head/runtime/components.ts @@ -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) }) diff --git a/packages/nuxt/src/head/runtime/composables.ts b/packages/nuxt/src/head/runtime/composables.ts index 8eaffa58283..d404b4f2f09 100644 --- a/packages/nuxt/src/head/runtime/composables.ts +++ b/packages/nuxt/src/head/runtime/composables.ts @@ -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) } diff --git a/packages/nuxt/src/head/runtime/lib/vue-meta.plugin.ts b/packages/nuxt/src/head/runtime/lib/vue-meta.plugin.ts index 9569534d954..ecdc4d689ae 100644 --- a/packages/nuxt/src/head/runtime/lib/vue-meta.plugin.ts +++ b/packages/nuxt/src/head/runtime/lib/vue-meta.plugin.ts @@ -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) 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 e1e6ebce866..c055d3ef62b 100644 --- a/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts +++ b/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts @@ -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 - }) - - 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.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() - if (process.server) { return } + // 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() + }) + }) + } - 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 diff --git a/packages/nuxt/src/head/runtime/plugin.ts b/packages/nuxt/src/head/runtime/plugin.ts index c537bc20382..5ad5c81ef62 100644 --- a/packages/nuxt/src/head/runtime/plugin.ts +++ b/packages/nuxt/src/head/runtime/plugin.ts @@ -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) { diff --git a/packages/schema/build.config.ts b/packages/schema/build.config.ts index 80bbf5fb7b2..82e22e2b494 100644 --- a/packages/schema/build.config.ts +++ b/packages/schema/build.config.ts @@ -22,6 +22,7 @@ export default defineBuildConfig({ 'vue-meta', 'vue-router', 'vue-bundle-renderer', + '@vueuse/head', 'vue', 'hookable', 'nitropack', diff --git a/packages/schema/package.json b/packages/schema/package.json index 553bc596110..ec8e035aa99 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -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" }, diff --git a/packages/schema/src/config/_app.ts b/packages/schema/src/config/_app.ts index 46adadd8948..176afd8ed12 100644 --- a/packages/schema/src/config/_app.ts +++ b/packages/schema/src/config/_app.ts @@ -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 */ diff --git a/packages/schema/src/types/config.ts b/packages/schema/src/types/config.ts index 129844dcc47..97f7e02581c 100644 --- a/packages/schema/src/types/config.ts +++ b/packages/schema/src/types/config.ts @@ -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 diff --git a/packages/schema/src/types/meta.ts b/packages/schema/src/types/meta.ts index 1ae2d75854d..d79bfe55b72 100644 --- a/packages/schema/src/types/meta.ts +++ b/packages/schema/src/types/meta.ts @@ -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'] } diff --git a/test/basic.test.ts b/test/basic.test.ts index d95d46ff83e..69843ffb232 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -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') expect(headHtml).not.toContain('') expect(headHtml).toContain('') @@ -252,7 +253,7 @@ describe('head tags', () => { expect(headHtml).toMatch(/]*class="html-attrs-test"/) expect(headHtml).toMatch(/]*class="body-attrs-test"/) expect(headHtml).toContain('script>console.log("works with useMeta too")') - expect(headHtml).toContain('') + expect(headHtml).toContain('') const indexHtml = await $fetch('/') // should render charset by default diff --git a/test/fixtures/basic/pages/head.vue b/test/fixtures/basic/pages/head.vue index 3221ba9effe..ee3328b8a3f 100644 --- a/test/fixtures/basic/pages/head.vue +++ b/test/fixtures/basic/pages/head.vue @@ -1,5 +1,6 @@ diff --git a/test/fixtures/basic/types.ts b/test/fixtures/basic/types.ts index 035a71abc87..d7a9320fa8d 100644 --- a/test/fixtures/basic/types.ts +++ b/test/fixtures/basic/types.ts @@ -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>() diff --git a/yarn.lock b/yarn.lock index b4c2e54a7d7..81c9f3fb114 100644 --- a/yarn.lock +++ b/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