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

This commit is contained in:
Harlan Wilton 2022-11-16 03:26:38 +11:00 committed by GitHub
parent f530cd7413
commit fddc8b2e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 107 additions and 211 deletions

View File

@ -4,11 +4,9 @@ description: useHead customizes the head properties of individual pages of your
# `useHead`
Nuxt provides the `useHead` composable to add and customize the head properties of individual pages of your Nuxt app. It uses [@vueuse/head](https://github.com/vueuse/head) under the hood.
Nuxt provides the `useHead` composable to add and customize the head properties of individual pages of your Nuxt app.
::alert{icon=👉}
`useHead` only works during `setup` or `Lifecycle Hooks`.
::
`useHead` is powered by [@vueuse/head](https://github.com/vueuse/head), you can find more in-depth documentation [here](https://unhead.harlanzw.com/)
::ReadMore{link="/getting-started/seo-meta"}
::
@ -19,7 +17,7 @@ Nuxt provides the `useHead` composable to add and customize the head properties
useHead(meta: MaybeComputedRef<MetaObject>): void
```
Below are the non-reactive types for `useMeta`. See [zhead](https://github.com/harlan-zw/zhead/tree/main/packages/schema/src) for more detailed types.
Below are the non-reactive types for `useHead`. See [zhead](https://github.com/harlan-zw/zhead/tree/main/packages/schema/src) for more detailed types.
```ts
interface MetaObject {

View File

@ -44,7 +44,9 @@
"@nuxt/vite-builder": "3.0.0-rc.13",
"@vue/reactivity": "^3.2.45",
"@vue/shared": "^3.2.45",
"@vueuse/head": "~1.0.0-rc.14",
"@vueuse/head": "^1.0.13",
"unhead": "^0.6.7",
"@unhead/ssr": "^0.6.7",
"chokidar": "^3.5.3",
"cookie-es": "^0.5.0",
"defu": "^6.1.0",

View File

@ -9,6 +9,8 @@ import defu from 'defu'
import fsExtra from 'fs-extra'
import { dynamicEventHandler } from 'h3'
import type { Plugin } from 'rollup'
import { createHeadCore } from 'unhead'
import { renderSSRHead } from '@unhead/ssr'
import { distDir } from '../dirs'
import { ImportProtectionPlugin } from './plugins/import-protection'
@ -122,6 +124,12 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
}
})
// Add head chunk for SPA renders
const head = createHeadCore()
head.push(nuxt.options.app.head)
const headChunk = await renderSSRHead(head)
nitroConfig.virtual!['#head-static'] = `export default ${JSON.stringify(headChunk)}`
// Add fallback server for `ssr: false`
if (!nuxt.options.ssr) {
nitroConfig.virtual!['#build/dist/server/server.mjs'] = 'export default () => {}'

View File

@ -41,6 +41,9 @@ const getClientManifest: () => Promise<Manifest> = () => import('#build/dist/ser
.then(r => r.default || r)
.then(r => typeof r === 'function' ? r() : r) as Promise<ClientManifest>
// @ts-ignore
const getStaticRenderedHead = () : Promise<NuxtMeta> => import('#head-static').then(r => r.default || r)
// @ts-ignore
const getServerEntry = () => import('#build/dist/server/server.mjs').then(r => r.default || r)
@ -102,7 +105,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
data: {},
state: {}
}
ssrContext!.renderMeta = ssrContext!.renderMeta ?? (() => ({}))
ssrContext!.renderMeta = ssrContext!.renderMeta ?? getStaticRenderedHead
return Promise.resolve(result)
}

View File

@ -1,5 +1,5 @@
import type { MetaObject } from '@nuxt/schema'
import type { MaybeComputedRef } from '@vueuse/head'
import type { HeadEntryOptions, UseHeadInput, ActiveHeadEntry } from '@vueuse/head'
import type { HeadAugmentations } from '@nuxt/schema'
import { useNuxtApp } from '#app'
/**
@ -9,6 +9,6 @@ import { useNuxtApp } from '#app'
* Alternatively, for reactive meta state, you can pass in a function
* that returns a meta object.
*/
export function useHead (meta: MaybeComputedRef<MetaObject>) {
useNuxtApp()._useHead(meta)
export function useHead<T extends HeadAugmentations> (input: UseHeadInput<T>, options?: HeadEntryOptions): ActiveHeadEntry<UseHeadInput<T>> | void {
return useNuxtApp()._useHead(input, options)
}

View File

@ -1,2 +1,6 @@
import type { UseHeadInput } from '@vueuse/head'
import type { HeadAugmentations } from '@nuxt/schema'
export * from './composables'
export type { MetaObject } from '@nuxt/schema'
export type MetaObject = UseHeadInput<HeadAugmentations>

View File

@ -1,63 +1,39 @@
import type { HeadEntryOptions, MaybeComputedRef } from '@vueuse/head'
import { createHead, renderHeadToString } from '@vueuse/head'
import { onBeforeUnmount, getCurrentInstance } from 'vue'
import type { MetaObject } from '@nuxt/schema'
import { defineNuxtPlugin, useRouter } from '#app'
import { createHead, useHead } from '@vueuse/head'
import { defineNuxtPlugin } from '#app'
// @ts-expect-error untyped
import { appHead } from '#build/nuxt.config.mjs'
export default defineNuxtPlugin((nuxtApp) => {
const head = createHead()
head.addEntry(appHead, { resolved: true })
head.push(appHead)
nuxtApp.vueApp.use(head)
if (process.client) {
// pause dom updates until page is ready and between page transitions
let pauseDOMUpdates = true
head.hooks['before:dom'].push(() => !pauseDOMUpdates)
nuxtApp.hooks.hookOnce('app:mounted', () => {
const unpauseDom = () => {
pauseDOMUpdates = false
head.updateDOM()
// start pausing DOM updates when route changes (trigger immediately)
useRouter().beforeEach(() => {
pauseDOMUpdates = true
})
// watch for new route before unpausing dom updates (triggered after suspense resolved)
useRouter().afterEach(() => {
// only if we have paused (clicking on a link to the current route triggers this)
if (pauseDOMUpdates) {
pauseDOMUpdates = false
head.updateDOM()
}
})
})
}
nuxtApp._useHead = (_meta: MaybeComputedRef<MetaObject>, options: HeadEntryOptions) => {
if (process.server) {
head.addEntry(_meta, options)
return
// triggers dom update
head.internalHooks.callHook('entries:updated', head.unhead)
}
const cleanUp = head.addReactiveEntry(_meta, options)
const vm = getCurrentInstance()
if (!vm) { return }
onBeforeUnmount(() => {
cleanUp()
head.updateDOM()
})
head.internalHooks.hook('dom:beforeRender', (context) => { context.shouldRender = !pauseDOMUpdates })
nuxtApp.hooks.hook('page:start', () => { pauseDOMUpdates = true })
// wait for new page before unpausing dom updates (triggered after suspense resolved)
nuxtApp.hooks.hook('page:finish', unpauseDom)
nuxtApp.hooks.hook('app:mounted', unpauseDom)
}
// useHead does not depend on a vue component context, we keep it on the nuxtApp for backwards compatibility
nuxtApp._useHead = useHead
if (process.server) {
nuxtApp.ssrContext!.renderMeta = async () => {
const meta = await renderHeadToString(head)
const { renderSSRHead } = await import('@unhead/ssr')
const meta = await renderSSRHead(head.unhead)
return {
...meta,
bodyScriptsPrepend: meta.bodyTagsOpen,
// resolves naming difference with NuxtMeta and @vueuse/head
bodyScripts: meta.bodyTags
}

View File

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

View File

@ -17,7 +17,7 @@
"@types/lodash.template": "^4",
"@types/semver": "^7",
"@vitejs/plugin-vue": "^3.2.0",
"@vueuse/head": "~1.0.0-rc.14",
"@unhead/schema": "^0.6.7",
"nitropack": "^1.0.0-0",
"unbuild": "latest",
"vite": "~3.2.3"

View File

@ -1,6 +1,6 @@
import type { HeadObjectPlain, HeadObject } from '@vueuse/head'
import type { Head, MergeHead } from '@unhead/schema'
export interface HeadAugmentations {
export interface HeadAugmentations extends MergeHead {
// runtime type modifications
base?: {}
link?: {}
@ -12,7 +12,8 @@ export interface HeadAugmentations {
bodyAttrs?: {}
}
export type MetaObjectRaw = HeadObjectPlain<HeadAugmentations>
export type MetaObjectRaw = Head<HeadAugmentations>
export type MetaObject = MetaObjectRaw
export type AppHeadMetaObject = MetaObjectRaw & {
/**
@ -29,70 +30,3 @@ export type AppHeadMetaObject = MetaObjectRaw & {
*/
viewport?: string
}
export interface MetaObject {
/**
* The <title> HTML element defines the document's title that is shown in a browser's title bar or a page's tab.
* It only contains text; tags within the element are ignored.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title
*/
title?: HeadObject<HeadAugmentations>['title']
/**
* Generate the title from a template.
*/
titleTemplate?: HeadObject<HeadAugmentations>['titleTemplate']
/**
* The <base> HTML element specifies the base URL to use for all relative URLs in a document.
* There can be only one <base> element in a document.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
*/
base?: HeadObject<HeadAugmentations>['base']
/**
* The <link> HTML element specifies relationships between the current document and an external resource.
* This element is most commonly used to link to stylesheets, but is also used to establish site icons
* (both "favicon" style icons and icons for the home screen and apps on mobile devices) among other things.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as
*/
link?: HeadObject<HeadAugmentations>['link']
/**
* The <meta> element represents metadata that cannot be expressed in other HTML elements, like <link> or <script>.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta
*/
meta?: HeadObject<HeadAugmentations>['meta']
/**
* The <style> HTML element contains style information for a document, or part of a document.
* It contains CSS, which is applied to the contents of the document containing the <style> element.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
*/
style?: HeadObject<HeadAugmentations>['style']
/**
* The <script> HTML element is used to embed executable code or data; this is typically used to embed or refer to JavaScript code.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
*/
script?: HeadObject<HeadAugmentations>['script']
/**
* The <noscript> HTML element defines a section of HTML to be inserted if a script type on the page is unsupported
* or if scripting is currently turned off in the browser.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript
*/
noscript?: HeadObject<HeadAugmentations>['noscript']
/**
* Attributes for the <html> HTML element.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html
*/
htmlAttrs?: HeadObject<HeadAugmentations>['htmlAttrs']
/**
* Attributes for the <body> HTML element.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/body
*/
bodyAttrs?: HeadObject<HeadAugmentations>['bodyAttrs']
}

View File

@ -417,9 +417,10 @@ importers:
'@nuxt/vite-builder': workspace:*
'@types/fs-extra': ^9.0.13
'@types/hash-sum': ^1.0.0
'@unhead/ssr': ^0.6.7
'@vue/reactivity': ^3.2.45
'@vue/shared': ^3.2.45
'@vueuse/head': ~1.0.0-rc.14
'@vueuse/head': ^1.0.13
chokidar: ^3.5.3
cookie-es: ^0.5.0
defu: ^6.1.0
@ -447,6 +448,7 @@ importers:
unbuild: ^0.9.4
unctx: ^2.0.2
unenv: ^1.0.0
unhead: ^0.6.7
unimport: ^1.0.0
unplugin: ^0.10.2
untyped: ^0.5.0
@ -462,9 +464,10 @@ importers:
'@nuxt/telemetry': 2.1.6
'@nuxt/ui-templates': 0.4.0
'@nuxt/vite-builder': link:../vite
'@unhead/ssr': 0.6.7
'@vue/reactivity': 3.2.45
'@vue/shared': 3.2.45
'@vueuse/head': 1.0.0-rc.14_vue@3.2.45
'@vueuse/head': 1.0.13_vue@3.2.45
chokidar: 3.5.3
cookie-es: 0.5.0
defu: 6.1.0
@ -491,6 +494,7 @@ importers:
ultrahtml: 1.0.0
unctx: 2.0.2
unenv: 1.0.0
unhead: 0.6.7
unimport: 1.0.0
unplugin: 0.10.2
untyped: 0.5.0
@ -508,8 +512,8 @@ importers:
specifiers:
'@types/lodash.template': ^4
'@types/semver': ^7
'@unhead/schema': ^0.6.7
'@vitejs/plugin-vue': ^3.2.0
'@vueuse/head': ~1.0.0-rc.14
c12: ^1.0.1
create-require: ^1.1.1
defu: ^6.1.0
@ -541,8 +545,8 @@ importers:
devDependencies:
'@types/lodash.template': 4.5.1
'@types/semver': 7.3.12
'@unhead/schema': 0.6.7
'@vitejs/plugin-vue': 3.2.0_vite@3.2.3
'@vueuse/head': 1.0.0-rc.14
nitropack: 1.0.0-0
unbuild: 0.9.4
vite: 3.2.3
@ -2099,6 +2103,34 @@ packages:
eslint-visitor-keys: 3.3.0
dev: true
/@unhead/dom/0.6.7:
resolution: {integrity: sha512-HUp8ygfQ7VfpEouy26nLsuk6Y5ii5UldoC1NdTuzHSxkYwlq0v4rMpiO1f7T/0i9D7RJDpAp8LdhsHKgcEmRiA==}
dependencies:
'@unhead/schema': 0.6.7
dev: false
/@unhead/schema/0.6.7:
resolution: {integrity: sha512-wVf3Zu7deua63J/HYKoUXUkG0wZLPnKws1DNpSJkhNrjbpdzvsfI08lPIkW1ISqddQiLFXOgcnbhYrhcD7ZZYw==}
dependencies:
'@zhead/schema': 1.0.0-beta.13
hookable: 5.4.1
/@unhead/ssr/0.6.7:
resolution: {integrity: sha512-jvEJER7dMSEtI6u60OrEz4gwhmQDGbo086HXdIYoT7RoaJ/YQa/CUAHRfLTO6F657iYs42VPFM3yRCVUbexZhA==}
dependencies:
'@unhead/schema': 0.6.7
dev: false
/@unhead/vue/0.6.7_vue@3.2.45:
resolution: {integrity: sha512-9qQ90WtOoQpXihM/MzDHiLWZ53HcZg9Xynf8PhY+004wLfj9e2cE2jSms1mjdP+BC7wVoeENQ9NCv4fOa6l6MQ==}
peerDependencies:
vue: '>=2.7 || >=3'
dependencies:
'@unhead/schema': 0.6.7
hookable: 5.4.1
vue: 3.2.45
dev: false
/@unocss/astro/0.45.25:
resolution: {integrity: sha512-TxFxESIvSZtaVIRP/cDyrwnYlHz78mG9ohhAhI0adLGi0v0yBwoGJ1krlqxAuTzhV191InTlwD/C5QevTsf86Q==}
dependencies:
@ -2562,29 +2594,16 @@ packages:
- vue
dev: true
/@vueuse/head/1.0.0-rc.14:
resolution: {integrity: sha512-3DtOfSE1141IKPIq4AR5UXQZPWQFSd7E5f3M+HkBRyxWsyxbNBBmK5hqkSYc2ENoFXa3xPhLYZXJPKuxqfJmiA==}
/@vueuse/head/1.0.13_vue@3.2.45:
resolution: {integrity: sha512-QJ69f+V0blbroHPuOAYBQZek31kMmFttgUj+QmL+rnXgFUfRXj5rqIDC2chFBSsjHcWl6KfzwPbou5mjCVJAgw==}
peerDependencies:
vue: '>=2.7 || >=3'
dependencies:
'@vueuse/shared': 9.4.0
'@zhead/schema': 0.9.9
'@zhead/schema-vue': 0.9.9
transitivePeerDependencies:
- '@vue/composition-api'
dev: true
/@vueuse/head/1.0.0-rc.14_vue@3.2.45:
resolution: {integrity: sha512-3DtOfSE1141IKPIq4AR5UXQZPWQFSd7E5f3M+HkBRyxWsyxbNBBmK5hqkSYc2ENoFXa3xPhLYZXJPKuxqfJmiA==}
peerDependencies:
vue: '>=2.7 || >=3'
dependencies:
'@vueuse/shared': 9.4.0_vue@3.2.45
'@zhead/schema': 0.9.9
'@zhead/schema-vue': 0.9.9_vue@3.2.45
'@unhead/dom': 0.6.7
'@unhead/schema': 0.6.7
'@unhead/ssr': 0.6.7
'@unhead/vue': 0.6.7_vue@3.2.45
vue: 3.2.45
transitivePeerDependencies:
- '@vue/composition-api'
dev: false
/@vueuse/integrations/9.3.0_focus-trap@7.0.0:
@ -2660,24 +2679,6 @@ packages:
- vue
dev: true
/@vueuse/shared/9.4.0:
resolution: {integrity: sha512-fTuem51KwMCnqUKkI8B57qAIMcFovtGgsCtAeqxIzH3i6nE9VYge+gVfneNHAAy7lj8twbkNfqQSygOPJTm4tQ==}
dependencies:
vue-demi: 0.13.11
transitivePeerDependencies:
- '@vue/composition-api'
- vue
dev: true
/@vueuse/shared/9.4.0_vue@3.2.45:
resolution: {integrity: sha512-fTuem51KwMCnqUKkI8B57qAIMcFovtGgsCtAeqxIzH3i6nE9VYge+gVfneNHAAy7lj8twbkNfqQSygOPJTm4tQ==}
dependencies:
vue-demi: 0.13.11_vue@3.2.45
transitivePeerDependencies:
- '@vue/composition-api'
- vue
dev: false
/@webassemblyjs/ast/1.11.1:
resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
dependencies:
@ -2775,31 +2776,8 @@ packages:
/@xtuc/long/4.2.2:
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
/@zhead/schema-vue/0.9.9:
resolution: {integrity: sha512-f7sOPMc1zQJ+tDDWWaksNsGoGGuRv5aHvOdZvsL3dIxbiHVlGVhDi/HZbUUupCtlYAPv2D8E/tUmwWKh/UrbXw==}
peerDependencies:
vue: '>=2.7 || >=3'
dependencies:
'@vueuse/shared': 9.4.0
'@zhead/schema': 0.9.9
transitivePeerDependencies:
- '@vue/composition-api'
dev: true
/@zhead/schema-vue/0.9.9_vue@3.2.45:
resolution: {integrity: sha512-f7sOPMc1zQJ+tDDWWaksNsGoGGuRv5aHvOdZvsL3dIxbiHVlGVhDi/HZbUUupCtlYAPv2D8E/tUmwWKh/UrbXw==}
peerDependencies:
vue: '>=2.7 || >=3'
dependencies:
'@vueuse/shared': 9.4.0_vue@3.2.45
'@zhead/schema': 0.9.9
vue: 3.2.45
transitivePeerDependencies:
- '@vue/composition-api'
dev: false
/@zhead/schema/0.9.9:
resolution: {integrity: sha512-B/No5zsZB1gz6BT7OKcD0rbyZCGoF6ImeQm2ffupQrgUpYAIv/LGtn3RVNSOcX2R2DB4g79UtuIwK0OxugFjJQ==}
/@zhead/schema/1.0.0-beta.13:
resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==}
/abbrev/1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
@ -6737,7 +6715,7 @@ packages:
typescript:
optional: true
dependencies:
defu: 6.1.1
defu: 6.1.0
esbuild: 0.14.54
fs-extra: 10.1.0
globby: 11.1.0
@ -8730,6 +8708,14 @@ packages:
node-fetch-native: 1.0.1
pathe: 1.0.0
/unhead/0.6.7:
resolution: {integrity: sha512-YIVkGHHCUXbh0xFItqvJi08uk390ondXr+ge9lYwDiGQms8ykSGkDUbpMEXDrC8WnwoGXFkwaEZXRPIWx4ZxfA==}
dependencies:
'@unhead/dom': 0.6.7
'@unhead/schema': 0.6.7
hookable: 5.4.1
dev: false
/unimport/1.0.0:
resolution: {integrity: sha512-7M2+6uC6Ik3/imN0VhEBJGnnH5SWLPxhPAPKdMMIt2Bh+YW7F42aZFC9APW3h82r4bvS5qQWaMJko2G9m7SDYA==}
dependencies:
@ -9221,21 +9207,6 @@ packages:
optional: true
dev: true
/vue-demi/0.13.11_vue@3.2.45:
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
dependencies:
vue: 3.2.45
dev: false
/vue-devtools-stub/0.1.0:
resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==}
dev: false

View File

@ -253,7 +253,7 @@ describe('head tags', () => {
expect(headHtml).toContain('<meta name="description" content="overriding with an inline useHead call">')
expect(headHtml).toMatch(/<html[^>]*class="html-attrs-test"/)
expect(headHtml).toMatch(/<body[^>]*class="body-attrs-test"/)
expect(headHtml).toContain('<script src="https://a-body-appended-script.com" data-meta-body></script></body>')
expect(headHtml).toContain('<script src="https://a-body-appended-script.com"></script></body>')
const indexHtml = await $fetch('/')
// should render charset by default

View File

@ -52,6 +52,9 @@ describe.skipIf(isWindows)('minimal nuxt application', () => {
expect(packages).toMatchInlineSnapshot(`
[
"@babel/parser",
"@unhead/dom",
"@unhead/ssr",
"@unhead/vue",
"@vue/compiler-core",
"@vue/compiler-dom",
"@vue/compiler-ssr",
@ -60,7 +63,6 @@ describe.skipIf(isWindows)('minimal nuxt application', () => {
"@vue/runtime-dom",
"@vue/server-renderer",
"@vue/shared",
"@vueuse/shared",
"buffer-from",
"cookie-es",
"destr",
@ -81,7 +83,6 @@ describe.skipIf(isWindows)('minimal nuxt application', () => {
"unstorage",
"vue",
"vue-bundle-renderer",
"vue-demi",
]
`)
})

View File

@ -123,7 +123,6 @@ describe('runtimeConfig', () => {
describe('head', () => {
it('correctly types nuxt.config options', () => {
// @ts-expect-error
defineNuxtConfig({ app: { head: { titleTemplate: () => 'test' } } })
defineNuxtConfig({
app: {