mirror of
https://github.com/nuxt/nuxt.git
synced 2025-03-21 00:35:55 +00:00
feat(nuxt): upgrade to unhead v2 (#31169)
This commit is contained in:
parent
52146b4641
commit
4edd782011
@ -245,6 +245,69 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
#### Unhead v2
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
||||
##### What Changed
|
||||
|
||||
[Unhead](https://unhead.unjs.io/), used to generate `<head>` tags, has been updated to version 2. While mostly compatible it includes several breaking changes
|
||||
for lower-level APIs.
|
||||
|
||||
* Removed props: `vmid`, `hid`, `children`, `body`.
|
||||
* Promise input no longer supported.
|
||||
* Tags are now sorted using Capo.js by default.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
The above changes should have minimal impact on your app.
|
||||
|
||||
If you have issues you should verify:
|
||||
|
||||
* You're not using any of the removed props.
|
||||
|
||||
```diff
|
||||
useHead({
|
||||
meta: [{
|
||||
name: 'description',
|
||||
// meta tags don't need a vmid, or a key
|
||||
- vmid: 'description'
|
||||
- hid: 'description'
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
* If you're using [Template Params](https://unhead.unjs.io/docs/plugins/template-params) or [Alias Tag Sorting](https://unhead.unjs.io/docs/plugins/alias-sorting), you will need to explicitly opt in to these features now.
|
||||
|
||||
```ts
|
||||
import { TemplateParamsPlugin, AliasSortingPlugin } from '@unhead/vue/plugins'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
setup() {
|
||||
const unhead = injectHead()
|
||||
unhead.use(TemplateParamsPlugin)
|
||||
unhead.use(AliasSortingPlugin)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
While not required it's recommend to update any imports from `@unhead/vue` to `#imports` or `nuxt/app`.
|
||||
|
||||
```diff
|
||||
-import { useHead } from '@unhead/vue'
|
||||
+import { useHead } from '#imports'
|
||||
```
|
||||
|
||||
If you still have issues you may revert to the v1 behavior by enabling the `head.legacy` config.
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
unhead: {
|
||||
legacy: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### New DOM Location for SPA Loading Screen
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
@ -4,35 +4,61 @@ description: Improve your Nuxt app's SEO with powerful head config, composables
|
||||
navigation.icon: i-ph-file-search
|
||||
---
|
||||
|
||||
## Defaults
|
||||
Nuxt head tag management is powered by [Unhead](https://unhead.unjs.io). It provides sensible defaults, several powerful composables
|
||||
and numerous configuration options to manage your app's head and SEO meta tags.
|
||||
|
||||
Out-of-the-box, Nuxt provides sensible defaults, which you can override if needed.
|
||||
## Nuxt Config
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
app: {
|
||||
head: {
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width, initial-scale=1',
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Providing an [`app.head`](/docs/api/nuxt-config#head) property in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) allows you to customize the head for your entire app.
|
||||
Providing an [`app.head`](/docs/api/nuxt-config#head) property in your [`nuxt.config.ts`](/docs/guide/directory-structure/nuxt-config) allows you to statically customize the head for your entire app.
|
||||
|
||||
::important
|
||||
This method does not allow you to provide reactive data. We recommend to use `useHead()` in `app.vue`.
|
||||
::
|
||||
|
||||
Shortcuts are available to make configuration easier: `charset` and `viewport`. You can also provide any of the keys listed below in [Types](#types).
|
||||
It's good practice to set tags here that won't change such as your site title default, language and favicon.
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
app: {
|
||||
head: {
|
||||
title: 'Nuxt', // default fallback title
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
You can also provide any of the keys listed below in [Types](#types).
|
||||
|
||||
### Defaults Tags
|
||||
|
||||
Some tags are provided by Nuxt by default to ensure your website works well out of the box.
|
||||
|
||||
- `viewport`: `width=device-width, initial-scale=1`
|
||||
- `charset`: `utf-8`
|
||||
|
||||
While most sites won't need to override these defaults, you can update them using the keyed shortcuts.
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
app: {
|
||||
head: {
|
||||
// update Nuxt defaults
|
||||
charset: 'utf-16',
|
||||
viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## `useHead`
|
||||
|
||||
The [`useHead`](/docs/api/composables/use-head) composable function allows you to manage your head tags programmatically and reactively,
|
||||
powered by [Unhead](https://unhead.unjs.io).
|
||||
|
||||
As with all composables, it can only be used with a components `setup` and lifecycle hooks.
|
||||
The [`useHead`](/docs/api/composables/use-head) composable function supports reactive input, allowing you to manage your head tags programmatically.
|
||||
|
||||
```vue twoslash [app.vue]
|
||||
<script setup lang="ts">
|
||||
@ -53,7 +79,7 @@ We recommend to take a look at the [`useHead`](/docs/api/composables/use-head) a
|
||||
|
||||
## `useSeoMeta`
|
||||
|
||||
The [`useSeoMeta`](/docs/api/composables/use-seo-meta) composable lets you define your site's SEO meta tags as a flat object with full TypeScript support.
|
||||
The [`useSeoMeta`](/docs/api/composables/use-seo-meta) composable lets you define your site's SEO meta tags as an object with full type safety.
|
||||
|
||||
This helps you avoid typos and common mistakes, such as using `name` instead of `property`.
|
||||
|
||||
|
@ -28,8 +28,8 @@ useHeadSafe({
|
||||
// <meta content="0;javascript:alert(1)">
|
||||
```
|
||||
|
||||
::read-more{to="https://unhead.unjs.io/usage/composables/use-head-safe" target="_blank"}
|
||||
Read more on `unhead` documentation.
|
||||
::read-more{to="https://unhead.unjs.io/docs/api/use-head-safe" target="_blank"}
|
||||
Read more on the `Unhead` documentation.
|
||||
::
|
||||
|
||||
## Type
|
||||
@ -41,13 +41,14 @@ useHeadSafe(input: MaybeComputedRef<HeadSafe>): void
|
||||
The list of allowed values is:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
htmlAttrs: ['id', 'class', 'lang', 'dir'],
|
||||
bodyAttrs: ['id', 'class'],
|
||||
meta: ['id', 'name', 'property', 'charset', 'content'],
|
||||
noscript: ['id', 'textContent'],
|
||||
script: ['id', 'type', 'textContent'],
|
||||
link: ['id', 'color', 'crossorigin', 'fetchpriority', 'href', 'hreflang', 'imagesrcset', 'imagesizes', 'integrity', 'media', 'referrerpolicy', 'rel', 'sizes', 'type'],
|
||||
const WhitelistAttributes = {
|
||||
htmlAttrs: ['class', 'style', 'lang', 'dir'],
|
||||
bodyAttrs: ['class', 'style'],
|
||||
meta: ['name', 'property', 'charset', 'content', 'media'],
|
||||
noscript: ['textContent'],
|
||||
style: ['media', 'textContent', 'nonce', 'title', 'blocking'],
|
||||
script: ['type', 'textContent', 'nonce', 'blocking'],
|
||||
link: ['color', 'crossorigin', 'fetchpriority', 'href', 'hreflang', 'imagesrcset', 'imagesizes', 'integrity', 'media', 'referrerpolicy', 'rel', 'sizes', 'type'],
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -17,7 +17,7 @@ This composable is available in Nuxt v3.12+.
|
||||
## Description
|
||||
|
||||
A composable which observes the page title changes and updates the announcer message accordingly. Used by [`<NuxtRouteAnnouncer>`](/docs/api/components/nuxt-route-announcer) and controllable.
|
||||
It hooks into Unhead's [`dom:rendered`](https://unhead.unjs.io/api/core/hooks#dom-hooks) to read the page's title and set it as the announcer message.
|
||||
It hooks into Unhead's [`dom:rendered`](https://unhead.unjs.io/docs/guides/hooks) to read the page's title and set it as the announcer message.
|
||||
|
||||
## Parameters
|
||||
|
||||
|
@ -49,3 +49,32 @@ useSeoMeta({
|
||||
There are over 100 parameters. See the [full list of parameters in the source code](https://github.com/harlan-zw/zhead/blob/main/packages/zhead/src/metaFlat.ts#L1035).
|
||||
|
||||
:read-more{to="/docs/getting-started/seo-meta"}
|
||||
|
||||
## Performance
|
||||
|
||||
In most instances, SEO meta tags don't need to be reactive as search engine robots primarily scan the initial page load.
|
||||
|
||||
For better performance, you can wrap your `useSeoMeta` calls in a server-only condition when the meta tags don't need to be reactive:
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
if (import.meta.server) {
|
||||
// These meta tags will only be added during server-side rendering
|
||||
useSeoMeta({
|
||||
robots: 'index, follow',
|
||||
description: 'Static description that does not need reactivity',
|
||||
ogImage: 'https://example.com/image.png',
|
||||
// other static meta tags...
|
||||
})
|
||||
}
|
||||
|
||||
const dynamicTitle = ref('My title')
|
||||
// Only use reactive meta tags outside the condition when necessary
|
||||
useSeoMeta({
|
||||
title: () => dynamicTitle.value,
|
||||
ogTitle: () => dynamicTitle.value,
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
This previously used the [`useServerSeoMeta`](/docs/api/composables/use-server-seo-meta) composable, but it has been deprecated in favor of this approach.
|
||||
|
@ -111,7 +111,8 @@ export default defineNuxtModule({
|
||||
```
|
||||
|
||||
```ts [plugin.ts]
|
||||
import { createHead as createClientHead, createServerHead } from '@unhead/vue'
|
||||
import { createHead as createServerHead } from '@unhead/vue/server'
|
||||
import { createHead as createClientHead } from '@unhead/vue/client'
|
||||
import { defineNuxtPlugin } from '#imports'
|
||||
// @ts-ignore
|
||||
import metaConfig from '#build/meta.config.mjs'
|
||||
|
@ -11,7 +11,7 @@ Nuxt 3 provides several different ways to manage your meta tags:
|
||||
You can customize `title`, `titleTemplate`, `base`, `script`, `noscript`, `style`, `meta`, `link`, `htmlAttrs` and `bodyAttrs`.
|
||||
|
||||
::tip
|
||||
Nuxt currently uses [`vueuse/head`](https://github.com/vueuse/head) to manage your meta tags, but implementation details may change.
|
||||
Nuxt currently uses [`Unhead`](https://github.com/unjs/unhead) to manage your meta tags, but implementation details may change.
|
||||
::
|
||||
|
||||
:read-more{to="/docs/getting-started/seo-meta"}
|
||||
|
10
package.json
10
package.json
@ -46,11 +46,7 @@
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@types/node": "22.13.9",
|
||||
"@unhead/dom": "1.11.20",
|
||||
"@unhead/schema": "1.11.20",
|
||||
"@unhead/shared": "1.11.20",
|
||||
"@unhead/ssr": "1.11.20",
|
||||
"@unhead/vue": "1.11.20",
|
||||
"@unhead/vue": "2.0.0-rc.1",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
@ -66,7 +62,6 @@
|
||||
"typescript": "5.8.2",
|
||||
"ufo": "1.5.4",
|
||||
"unbuild": "3.5.0",
|
||||
"unhead": "1.11.20",
|
||||
"unimport": "4.1.2",
|
||||
"vite": "6.2.0",
|
||||
"vue": "3.5.13"
|
||||
@ -87,8 +82,7 @@
|
||||
"@types/babel__helper-plugin-utils": "7.10.3",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/semver": "7.5.8",
|
||||
"@unhead/schema": "1.11.20",
|
||||
"@unhead/vue": "1.11.20",
|
||||
"@unhead/vue": "2.0.0-rc.1",
|
||||
"@vitest/coverage-v8": "3.0.7",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"acorn": "8.14.0",
|
||||
|
@ -71,10 +71,7 @@
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/telemetry": "^2.6.5",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@unhead/dom": "^1.11.20",
|
||||
"@unhead/shared": "^1.11.20",
|
||||
"@unhead/ssr": "^1.11.20",
|
||||
"@unhead/vue": "^1.11.20",
|
||||
"@unhead/vue": "^2.0.0-rc.1",
|
||||
"@vue/shared": "^3.5.13",
|
||||
"c12": "^3.0.2",
|
||||
"chokidar": "^4.0.3",
|
||||
@ -120,7 +117,6 @@
|
||||
"uncrypto": "^0.1.3",
|
||||
"unctx": "^2.4.1",
|
||||
"unenv": "^1.10.0",
|
||||
"unhead": "^1.11.20",
|
||||
"unimport": "^4.1.2",
|
||||
"unplugin": "^2.2.0",
|
||||
"unplugin-vue-router": "^0.12.0",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { defineAsyncComponent } from 'vue'
|
||||
import { createVNode, defineComponent, onErrorCaptured } from 'vue'
|
||||
|
||||
import { injectHead } from '@unhead/vue'
|
||||
import { injectHead } from '../composables/head'
|
||||
import { createError } from '../composables/error'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
|
@ -3,7 +3,7 @@ import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineCom
|
||||
import { debounce } from 'perfect-debounce'
|
||||
import { hash } from 'ohash'
|
||||
import { appendResponseHeader } from 'h3'
|
||||
import { type ActiveHeadEntry, type Head, injectHead } from '@unhead/vue'
|
||||
import type { ActiveHeadEntry, SerializableHead } from '@unhead/vue'
|
||||
import { randomUUID } from 'uncrypto'
|
||||
import { joinURL, withQuery } from 'ufo'
|
||||
import type { FetchResponse } from 'ofetch'
|
||||
@ -11,6 +11,7 @@ import type { FetchResponse } from 'ofetch'
|
||||
import type { NuxtIslandResponse } from '../types'
|
||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||
import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
|
||||
import { injectHead } from '../composables/head'
|
||||
import { getFragmentHTML } from './utils'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
@ -90,7 +91,7 @@ export default defineComponent({
|
||||
const instance = getCurrentInstance()!
|
||||
const event = useRequestEvent()
|
||||
|
||||
let activeHead: ActiveHeadEntry<Head>
|
||||
let activeHead: ActiveHeadEntry<SerializableHead>
|
||||
|
||||
// TODO: remove use of `$fetch.raw` when nitro 503 issues on windows dev server are resolved
|
||||
const eventFetch = import.meta.server ? event!.fetch : import.meta.dev ? $fetch.raw : globalThis.fetch
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { getCurrentInstance, reactive, toRefs } from 'vue'
|
||||
import type { DefineComponent, defineComponent } from 'vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { hash } from 'ohash'
|
||||
import type { NuxtApp } from '../nuxt'
|
||||
import { getNuxtAppCtx, useNuxtApp } from '../nuxt'
|
||||
import { useHead } from './head'
|
||||
import { useAsyncData } from './asyncData'
|
||||
import { useRoute } from './router'
|
||||
import { createError } from './error'
|
||||
|
9
packages/nuxt/src/app/composables/head.ts
Normal file
9
packages/nuxt/src/app/composables/head.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export {
|
||||
injectHead,
|
||||
useHead,
|
||||
useServerHead,
|
||||
useSeoMeta,
|
||||
useServerSeoMeta,
|
||||
useHeadSafe,
|
||||
useServerHeadSafe,
|
||||
} from '#unhead/composables'
|
@ -1,17 +1,3 @@
|
||||
import type { UseHeadInput } from '@unhead/vue'
|
||||
import type { HeadAugmentations } from 'nuxt/schema'
|
||||
|
||||
/** @deprecated Use `UseHeadInput` from `@unhead/vue` instead. This may be removed in a future minor version. */
|
||||
export type MetaObject = UseHeadInput<HeadAugmentations>
|
||||
export {
|
||||
/** @deprecated Import `useHead` from `#imports` instead. This may be removed in a future minor version. */
|
||||
useHead,
|
||||
/** @deprecated Import `useSeoMeta` from `#imports` instead. This may be removed in a future minor version. */
|
||||
useSeoMeta,
|
||||
/** @deprecated Import `useServerSeoMeta` from `#imports` instead. This may be removed in a future minor version. */
|
||||
useServerSeoMeta,
|
||||
} from '@unhead/vue'
|
||||
|
||||
export { defineNuxtComponent } from './component'
|
||||
export { useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData } from './asyncData'
|
||||
export type { AsyncDataOptions, AsyncData, AsyncDataRequestStatus } from './asyncData'
|
||||
@ -40,3 +26,4 @@ export { useId } from './id'
|
||||
export { useRouteAnnouncer } from './route-announcer'
|
||||
export type { Politeness } from './route-announcer'
|
||||
export { useRuntimeHook } from './runtime-hook'
|
||||
export { injectHead, useHead, useHeadSafe, useSeoMeta, useServerHead, useServerHeadSafe, useServerSeoMeta } from './head'
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { hasProtocol, joinURL, withoutTrailingSlash } from 'ufo'
|
||||
import { parse } from 'devalue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { getCurrentInstance, onServerPrefetch, reactive } from 'vue'
|
||||
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
|
||||
import type { NuxtPayload } from '../nuxt'
|
||||
import { useHead } from './head'
|
||||
|
||||
import { useRoute } from './router'
|
||||
import { getAppManifest, getRouteRules } from './manifest'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { getCurrentScope, onScopeDispose, ref } from 'vue'
|
||||
import { injectHead } from '@unhead/vue'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { injectHead } from './head'
|
||||
|
||||
export type Politeness = 'assertive' | 'polite' | 'off'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { UseScriptInput } from '@unhead/vue'
|
||||
import type { UseScriptInput } from '@unhead/vue/scripts'
|
||||
import { createError } from './error'
|
||||
|
||||
function renderStubMessage (name: string) {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import type { H3Event } from 'h3'
|
||||
import { setResponseStatus as _setResponseStatus, appendHeader, getRequestHeader, getRequestHeaders, getResponseHeader, removeResponseHeader, setResponseHeader } from 'h3'
|
||||
import { computed, getCurrentInstance, ref } from 'vue'
|
||||
import { useServerHead } from '@unhead/vue'
|
||||
import type { H3Event$Fetch } from 'nitro/types'
|
||||
|
||||
import type { NuxtApp } from '../nuxt'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { toArray } from '../utils'
|
||||
import { useServerHead } from './head'
|
||||
|
||||
/** @since 3.0.0 */
|
||||
export function useRequestEvent (nuxtApp: NuxtApp = useNuxtApp()) {
|
||||
@ -132,7 +132,7 @@ export function onPrehydrate (callback: string | ((el: HTMLElement) => void), ke
|
||||
|
||||
useServerHead({
|
||||
script: [{
|
||||
key: vm && key ? key : code,
|
||||
key: vm && key ? key : undefined,
|
||||
tagPosition: 'bodyClose',
|
||||
tagPriority: 'critical',
|
||||
innerHTML: code,
|
||||
|
@ -1,7 +1,7 @@
|
||||
export { applyPlugin, applyPlugins, callWithNuxt, createNuxtApp, defineAppConfig, defineNuxtPlugin, definePayloadPlugin, isNuxtPlugin, registerPluginHooks, tryUseNuxtApp, useNuxtApp, useRuntimeConfig } from './nuxt'
|
||||
export type { CreateOptions, NuxtApp, NuxtPayload, NuxtPluginIndicator, NuxtSSRContext, ObjectPlugin, Plugin, PluginEnvContext, PluginMeta, ResolvedPluginMeta, RuntimeNuxtHooks } from './nuxt'
|
||||
|
||||
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta, useRuntimeHook } from './composables/index'
|
||||
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useHeadSafe, useServerSeoMeta, useServerHeadSafe, useServerHead, useSeoMeta, injectHead, useRuntimeHook } from './composables/index'
|
||||
export type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, Politeness, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
|
||||
|
||||
export { defineNuxtLink } from './components/index'
|
||||
|
@ -9,7 +9,7 @@ import type { EventHandlerRequest, H3Event } from 'h3'
|
||||
import type { AppConfig, AppConfigInput, RuntimeConfig } from 'nuxt/schema'
|
||||
import type { RenderResponse } from 'nitro/types'
|
||||
import type { LogObject } from 'consola'
|
||||
import type { MergeHead, VueHeadClient } from '@unhead/vue'
|
||||
import type { VueHeadClient } from '@unhead/vue/types'
|
||||
|
||||
import type { NuxtAppLiterals } from 'nuxt/app'
|
||||
|
||||
@ -66,7 +66,7 @@ export interface NuxtSSRContext extends SSRContext {
|
||||
error?: boolean
|
||||
nuxt: _NuxtApp
|
||||
payload: Partial<NuxtPayload>
|
||||
head: VueHeadClient<MergeHead>
|
||||
head: VueHeadClient
|
||||
/** This is used solely to render runtime config with SPA renderer. */
|
||||
config?: Pick<RuntimeConfig, 'public' | 'app'>
|
||||
teleports?: Record<string, string>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { defineNuxtPlugin } from '../nuxt'
|
||||
import { useHead } from '../composables/head'
|
||||
|
||||
const SUPPORTED_PROTOCOLS = ['http:', 'https:']
|
||||
|
||||
|
2
packages/nuxt/src/app/types/augments.d.ts
vendored
2
packages/nuxt/src/app/types/augments.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import type { UseHeadInput } from '@unhead/vue'
|
||||
import type { UseHeadInput } from '@unhead/vue/types'
|
||||
import type { NuxtApp, useNuxtApp } from '../nuxt.js'
|
||||
|
||||
declare global {
|
||||
|
@ -10,10 +10,9 @@ import type { H3Event } from 'h3'
|
||||
import { appendResponseHeader, createError, getQuery, getResponseStatus, getResponseStatusText, readBody, writeEarlyHints } from 'h3'
|
||||
import destr from 'destr'
|
||||
import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo'
|
||||
import { propsToString, renderSSRHead } from '@unhead/ssr'
|
||||
import type { Head, HeadEntryOptions } from '@unhead/schema'
|
||||
import type { Link, Script, Style } from '@unhead/vue'
|
||||
import { createServerHead, resolveUnrefHeadInput } from '@unhead/vue'
|
||||
import { createHead, propsToString, renderSSRHead } from '@unhead/vue/server'
|
||||
import { resolveUnrefHeadInput } from '@unhead/vue/utils'
|
||||
import type { HeadEntryOptions, Link, Script, SerializableHead, Style } from '@unhead/vue/types'
|
||||
|
||||
import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig } from 'nitro/runtime'
|
||||
import type { NuxtPayload, NuxtSSRContext } from 'nuxt/app'
|
||||
@ -23,7 +22,7 @@ import { islandCache, islandPropCache, payloadCache, sharedPrerenderCache } from
|
||||
|
||||
import { renderPayloadJsonScript, renderPayloadResponse, renderPayloadScript, splitPayload } from '../utils/payload'
|
||||
// @ts-expect-error virtual file
|
||||
import unheadPlugins from '#internal/unhead-plugins.mjs'
|
||||
import unheadOptions from '#internal/unhead-options.mjs'
|
||||
// @ts-expect-error virtual file
|
||||
import { renderSSRHeadOptions } from '#internal/unhead.config.mjs'
|
||||
|
||||
@ -76,7 +75,7 @@ export interface NuxtIslandContext {
|
||||
export interface NuxtIslandResponse {
|
||||
id?: string
|
||||
html: string
|
||||
head: Head
|
||||
head: SerializableHead
|
||||
props?: Record<string, Record<string, any>>
|
||||
components?: Record<string, NuxtIslandClientResponse>
|
||||
slots?: Record<string, NuxtIslandSlotResponse>
|
||||
@ -172,9 +171,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
// Get route options (currently to apply `ssr: false`)
|
||||
const routeOptions = getRouteRules(event)
|
||||
|
||||
const head = createServerHead({
|
||||
plugins: unheadPlugins,
|
||||
})
|
||||
const head = createHead(unheadOptions)
|
||||
|
||||
// needed for hash hydration plugin to work
|
||||
const headEntryOptions: HeadEntryOptions = { mode: 'server' }
|
||||
@ -315,14 +312,14 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
|
||||
// 3. Response for component islands
|
||||
if (isRenderingIsland && islandContext) {
|
||||
const islandHead: Head = {}
|
||||
for (const entry of head.headEntries()) {
|
||||
for (const [key, value] of Object.entries(resolveUnrefHeadInput(entry.input) as Head)) {
|
||||
const currentValue = islandHead[key as keyof Head]
|
||||
const islandHead: SerializableHead = {}
|
||||
for (const entry of head.entries.values()) {
|
||||
for (const [key, value] of Object.entries(resolveUnrefHeadInput(entry.input as any) as SerializableHead)) {
|
||||
const currentValue = islandHead[key as keyof SerializableHead]
|
||||
if (Array.isArray(currentValue)) {
|
||||
currentValue.push(...value)
|
||||
}
|
||||
islandHead[key as keyof Head] = value
|
||||
islandHead[key as keyof SerializableHead] = value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
import type { Manifest as ClientManifest } from 'vue-bundle-renderer'
|
||||
import type { Manifest } from 'vite'
|
||||
import { renderToString as _renderToString } from 'vue/server-renderer'
|
||||
import { propsToString } from '@unhead/ssr'
|
||||
import { propsToString } from '@unhead/vue/server'
|
||||
|
||||
import { useRuntimeConfig } from 'nitro/runtime'
|
||||
import type { NuxtSSRContext } from 'nuxt/app'
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { resolve } from 'pathe'
|
||||
import { addComponent, addImportsSources, addPlugin, addTemplate, defineNuxtModule, directoryToURL } from '@nuxt/kit'
|
||||
import { addBuildPlugin, addComponent, addPlugin, addTemplate, defineNuxtModule, directoryToURL } from '@nuxt/kit'
|
||||
import type { NuxtOptions } from '@nuxt/schema'
|
||||
import { resolveModulePath } from 'exsolve'
|
||||
import { distDir } from '../dirs'
|
||||
import { UnheadImportsPlugin } from './plugins/unhead-imports'
|
||||
|
||||
const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head', 'Html', 'Body']
|
||||
|
||||
@ -17,6 +18,7 @@ export default defineNuxtModule<NuxtOptions['unhead']>({
|
||||
// Transpile @unhead/vue
|
||||
nuxt.options.build.transpile.push('@unhead/vue')
|
||||
|
||||
const isNuxtV4 = nuxt.options._majorVersion === 4 || nuxt.options.future?.compatibilityVersion === 4
|
||||
// Register components
|
||||
const componentsPath = resolve(runtimeDir, 'components')
|
||||
for (const componentName of components) {
|
||||
@ -38,32 +40,34 @@ export default defineNuxtModule<NuxtOptions['unhead']>({
|
||||
]
|
||||
}
|
||||
|
||||
addImportsSources({
|
||||
from: '@unhead/vue',
|
||||
// hard-coded for now we so don't support auto-imports on the deprecated composables
|
||||
imports: [
|
||||
'injectHead',
|
||||
'useHead',
|
||||
'useSeoMeta',
|
||||
'useHeadSafe',
|
||||
'useServerHead',
|
||||
'useServerSeoMeta',
|
||||
'useServerHeadSafe',
|
||||
],
|
||||
})
|
||||
nuxt.options.alias['#unhead/composables'] = resolve(runtimeDir, 'composables', isNuxtV4 ? 'v4' : 'v3')
|
||||
addBuildPlugin(UnheadImportsPlugin({
|
||||
sourcemap: !!nuxt.options.sourcemap.server,
|
||||
rootDir: nuxt.options.rootDir,
|
||||
}))
|
||||
|
||||
// Opt-out feature allowing dependencies using @vueuse/head to work
|
||||
const importPaths = nuxt.options.modulesDir.map(d => directoryToURL(d))
|
||||
const unheadVue = resolveModulePath('@unhead/vue', { try: true, from: importPaths }) || '@unhead/vue'
|
||||
const unheadPlugins = resolveModulePath('@unhead/vue/plugins', { try: true, from: importPaths }) || '@unhead/vue/plugins'
|
||||
|
||||
addTemplate({
|
||||
filename: 'unhead-plugins.mjs',
|
||||
filename: 'unhead-options.mjs',
|
||||
getContents () {
|
||||
if (!nuxt.options.experimental.headNext) {
|
||||
return 'export default []'
|
||||
// disableDefaults is enabled to avoid server component issues
|
||||
if (isNuxtV4 && !options.legacy) {
|
||||
return `
|
||||
export default {
|
||||
disableDefaults: true,
|
||||
}`
|
||||
}
|
||||
return `import { CapoPlugin } from ${JSON.stringify(unheadVue)};
|
||||
export default import.meta.server ? [CapoPlugin({ track: true })] : [];`
|
||||
// v1 unhead legacy options
|
||||
const disableCapoSorting = !nuxt.options.experimental.headNext
|
||||
return `import { DeprecationsPlugin, PromisesPlugin, TemplateParamsPlugin, AliasSortingPlugin } from ${JSON.stringify(unheadPlugins)};
|
||||
export default {
|
||||
disableDefaults: true,
|
||||
disableCapoSorting: ${Boolean(disableCapoSorting)},
|
||||
plugins: [DeprecationsPlugin, PromisesPlugin, TemplateParamsPlugin, AliasSortingPlugin],
|
||||
}`
|
||||
},
|
||||
})
|
||||
|
||||
@ -78,7 +82,7 @@ export default import.meta.server ? [CapoPlugin({ track: true })] : [];`
|
||||
|
||||
// template is only exposed in nuxt context, expose in nitro context as well
|
||||
nuxt.hooks.hook('nitro:config', (config) => {
|
||||
config.virtual!['#internal/unhead-plugins.mjs'] = () => nuxt.vfs['#build/unhead-plugins.mjs']
|
||||
config.virtual!['#internal/unhead-options.mjs'] = () => nuxt.vfs['#build/unhead-options.mjs']
|
||||
config.virtual!['#internal/unhead.config.mjs'] = () => nuxt.vfs['#build/unhead.config.mjs']
|
||||
})
|
||||
|
||||
|
84
packages/nuxt/src/head/plugins/unhead-imports.ts
Normal file
84
packages/nuxt/src/head/plugins/unhead-imports.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import MagicString from 'magic-string'
|
||||
import type { Identifier, ImportSpecifier } from 'estree'
|
||||
import { normalize, relative } from 'pathe'
|
||||
import { unheadVueComposablesImports } from '@unhead/vue'
|
||||
import { genImport } from 'knitwork'
|
||||
import { parseAndWalk, withLocations } from '../../core/utils/parse'
|
||||
import { isJS, isVue } from '../../core/utils'
|
||||
import { distDir } from '../../dirs'
|
||||
import { logger } from '../../utils'
|
||||
|
||||
interface UnheadImportsPluginOptions {
|
||||
sourcemap: boolean
|
||||
rootDir: string
|
||||
}
|
||||
|
||||
const UNHEAD_LIB_RE = /node_modules[/\\](?:@unhead[/\\][^/\\]+|unhead)[/\\]/
|
||||
|
||||
function toImports (specifiers: ImportSpecifier[]) {
|
||||
return specifiers.map((specifier) => {
|
||||
const imported = specifier.imported as Identifier | null
|
||||
const isNamedImport = imported && imported.name !== specifier.local.name
|
||||
return isNamedImport ? `${imported.name} as ${specifier.local.name}` : specifier.local.name
|
||||
})
|
||||
}
|
||||
|
||||
const UnheadVue = '@unhead/vue'
|
||||
|
||||
/**
|
||||
* To use composable in an async context we need to pass Nuxt context to the Unhead composables.
|
||||
*
|
||||
* We swap imports from @unhead/vue to #app/composables/head and warn users for type safety.
|
||||
*/
|
||||
export const UnheadImportsPlugin = (options: UnheadImportsPluginOptions) => createUnplugin(() => {
|
||||
return {
|
||||
name: 'nuxt:head:unhead-imports',
|
||||
enforce: 'post',
|
||||
transformInclude (id) {
|
||||
id = normalize(id)
|
||||
return (
|
||||
(isJS(id) || isVue(id, { type: ['script'] })) &&
|
||||
!id.startsWith('virtual:') &&
|
||||
!id.startsWith(normalize(distDir)) &&
|
||||
!UNHEAD_LIB_RE.test(id)
|
||||
)
|
||||
},
|
||||
transform (code, id) {
|
||||
if (!code.includes(UnheadVue)) {
|
||||
return
|
||||
}
|
||||
const s = new MagicString(code)
|
||||
const importsToAdd: ImportSpecifier[] = []
|
||||
parseAndWalk(code, id, function (node) {
|
||||
if (node.type === 'ImportDeclaration' && [UnheadVue, '#app/composables/head'].includes(String(node.source.value))) {
|
||||
importsToAdd.push(...node.specifiers as ImportSpecifier[])
|
||||
const { start, end } = withLocations(node)
|
||||
s.remove(start, end)
|
||||
}
|
||||
})
|
||||
|
||||
const importsFromUnhead = importsToAdd.filter(specifier => unheadVueComposablesImports[UnheadVue].includes((specifier.imported as Identifier)?.name))
|
||||
const importsFromHead = importsToAdd.filter(specifier => !unheadVueComposablesImports[UnheadVue].includes((specifier.imported as Identifier)?.name))
|
||||
if (importsFromUnhead.length) {
|
||||
// warn if user has imported from @unhead/vue themselves
|
||||
if (!normalize(id).includes('node_modules')) {
|
||||
logger.warn(`You are importing from \`${UnheadVue}\` in \`./${relative(normalize(options.rootDir), normalize(id))}\`. Please import from \`#imports\` instead for full type safety.`)
|
||||
}
|
||||
s.prepend(`${genImport('#app/composables/head', toImports(importsFromUnhead))}\n`)
|
||||
}
|
||||
if (importsFromHead.length) {
|
||||
s.prepend(`${genImport(UnheadVue, toImports(importsFromHead))}\n`)
|
||||
}
|
||||
|
||||
if (s.hasChanged()) {
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: options.sourcemap
|
||||
? s.generateMap({ hires: true })
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
@ -1,6 +1,5 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import type { PropType, SetupContext } from 'vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import type {
|
||||
CrossOrigin,
|
||||
FetchPriority,
|
||||
@ -10,6 +9,7 @@ import type {
|
||||
ReferrerPolicy,
|
||||
Target,
|
||||
} from './types'
|
||||
import { useHead } from '#app/composables/head'
|
||||
|
||||
const removeUndefinedProps = (props: Props) => {
|
||||
const filteredProps = Object.create(null)
|
||||
|
72
packages/nuxt/src/head/runtime/composables/v3.ts
Normal file
72
packages/nuxt/src/head/runtime/composables/v3.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type { ActiveHeadEntry, UseHeadInput, UseHeadOptions, UseHeadSafeInput, UseSeoMetaInput, VueHeadClient } from '@unhead/vue'
|
||||
import { hasInjectionContext, inject } from 'vue'
|
||||
import {
|
||||
useHead as headCore,
|
||||
useHeadSafe as headSafe,
|
||||
headSymbol,
|
||||
useSeoMeta as seoMeta, useServerHead as serverHead, useServerHeadSafe as serverHeadSafe,
|
||||
useServerSeoMeta as serverSeoMeta,
|
||||
} from '@unhead/vue'
|
||||
import { tryUseNuxtApp } from '#app/nuxt'
|
||||
import type { NuxtApp } from '#app/nuxt'
|
||||
|
||||
/**
|
||||
* Injects the head client from the Nuxt context or Vue inject.
|
||||
*
|
||||
* In Nuxt v3 this function will not throw an error if the context is missing.
|
||||
*/
|
||||
export function injectHead (nuxtApp?: NuxtApp): VueHeadClient {
|
||||
// Nuxt 4 will throw an error if the context is missing
|
||||
const nuxt = nuxtApp || tryUseNuxtApp()
|
||||
return nuxt?.ssrContext?.head || nuxt?.runWithContext(() => {
|
||||
if (hasInjectionContext()) {
|
||||
return inject<VueHeadClient>(headSymbol)!
|
||||
}
|
||||
}) as VueHeadClient
|
||||
}
|
||||
|
||||
interface NuxtUseHeadOptions extends UseHeadOptions {
|
||||
nuxt?: NuxtApp
|
||||
}
|
||||
|
||||
export function useHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadInput> | void {
|
||||
const head = injectHead(options.nuxt)
|
||||
if (head) {
|
||||
return headCore(input, { head, ...options }) as ActiveHeadEntry<UseHeadInput>
|
||||
}
|
||||
}
|
||||
|
||||
export function useHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadSafeInput> | void {
|
||||
const head = injectHead(options.nuxt)
|
||||
if (head) {
|
||||
return headSafe(input, { head, ...options }) as ActiveHeadEntry<UseHeadSafeInput>
|
||||
}
|
||||
}
|
||||
|
||||
export function useSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseSeoMetaInput> | void {
|
||||
const head = injectHead(options.nuxt)
|
||||
if (head) {
|
||||
return seoMeta(input, { head, ...options }) as ActiveHeadEntry<UseHeadInput>
|
||||
}
|
||||
}
|
||||
|
||||
export function useServerHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadInput> | void {
|
||||
const head = injectHead(options.nuxt)
|
||||
if (head) {
|
||||
return serverHead(input, { head, ...options }) as ActiveHeadEntry<UseHeadInput>
|
||||
}
|
||||
}
|
||||
|
||||
export function useServerHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadSafeInput> | void {
|
||||
const head = injectHead(options.nuxt)
|
||||
if (head) {
|
||||
return serverHeadSafe(input, { head, ...options }) as ActiveHeadEntry<UseHeadSafeInput>
|
||||
}
|
||||
}
|
||||
|
||||
export function useServerSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseSeoMetaInput> | void {
|
||||
const head = injectHead(options.nuxt)
|
||||
if (head) {
|
||||
return serverSeoMeta(input, { head, ...options }) as ActiveHeadEntry<UseSeoMetaInput>
|
||||
}
|
||||
}
|
72
packages/nuxt/src/head/runtime/composables/v4.ts
Normal file
72
packages/nuxt/src/head/runtime/composables/v4.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type { ActiveHeadEntry, UseHeadInput, UseHeadOptions, UseHeadSafeInput, UseSeoMetaInput, VueHeadClient } from '@unhead/vue/types'
|
||||
import { hasInjectionContext, inject } from 'vue'
|
||||
import {
|
||||
useHead as headCore,
|
||||
useHeadSafe as headSafe,
|
||||
headSymbol,
|
||||
useSeoMeta as seoMeta, useServerHead as serverHead, useServerHeadSafe as serverHeadSafe,
|
||||
useServerSeoMeta as serverSeoMeta,
|
||||
} from '@unhead/vue'
|
||||
import { useNuxtApp } from '#app/nuxt'
|
||||
import type { NuxtApp } from '#app/nuxt'
|
||||
|
||||
/**
|
||||
* Injects the head client from the Nuxt context or Vue inject.
|
||||
*/
|
||||
export function injectHead (nuxtApp?: NuxtApp): VueHeadClient {
|
||||
// Nuxt 4 will throw an error if the context is missing
|
||||
const nuxt = nuxtApp || useNuxtApp()
|
||||
return nuxt.ssrContext?.head || nuxt.runWithContext(() => {
|
||||
if (hasInjectionContext()) {
|
||||
const head = inject<VueHeadClient>(headSymbol)
|
||||
// should not be possible
|
||||
if (!head) {
|
||||
throw new Error('[nuxt] [unhead] Missing Unhead instance.')
|
||||
}
|
||||
return head
|
||||
}
|
||||
}) as VueHeadClient
|
||||
}
|
||||
|
||||
interface NuxtUseHeadOptions extends UseHeadOptions {
|
||||
nuxt?: NuxtApp
|
||||
}
|
||||
|
||||
export function useHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadInput> {
|
||||
const head = injectHead(options.nuxt)
|
||||
return headCore(input, { head, ...options }) as ActiveHeadEntry<UseHeadInput>
|
||||
}
|
||||
|
||||
export function useHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadSafeInput> {
|
||||
const head = injectHead(options.nuxt)
|
||||
return headSafe(input, { head, ...options }) as ActiveHeadEntry<UseHeadSafeInput>
|
||||
}
|
||||
|
||||
export function useSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseSeoMetaInput> {
|
||||
const head = injectHead(options.nuxt)
|
||||
return seoMeta(input, { head, ...options }) as ActiveHeadEntry<UseSeoMetaInput>
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `useHead` instead and wrap with `if (import.meta.server)`
|
||||
*/
|
||||
export function useServerHead (input: UseHeadInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadInput> {
|
||||
const head = injectHead(options.nuxt)
|
||||
return serverHead(input, { head, ...options }) as ActiveHeadEntry<UseHeadInput>
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `useHeadSafe` instead and wrap with `if (import.meta.server)`
|
||||
*/
|
||||
export function useServerHeadSafe (input: UseHeadSafeInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseHeadSafeInput> {
|
||||
const head = injectHead(options.nuxt)
|
||||
return serverHeadSafe(input, { head, ...options }) as ActiveHeadEntry<UseHeadSafeInput>
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `useSeoMeta` instead and wrap with `if (import.meta.server)`
|
||||
*/
|
||||
export function useServerSeoMeta (input: UseSeoMetaInput, options: NuxtUseHeadOptions = {}): ActiveHeadEntry<UseSeoMetaInput> {
|
||||
const head = injectHead(options.nuxt)
|
||||
return serverSeoMeta(input, { head, ...options }) as ActiveHeadEntry<UseSeoMetaInput>
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
import { createHead as createClientHead, setHeadInjectionHandler } from '@unhead/vue'
|
||||
import { renderDOMHead } from '@unhead/dom'
|
||||
import { defineNuxtPlugin, useNuxtApp } from '#app/nuxt'
|
||||
import { createHead as createClientHead, renderDOMHead } from '@unhead/vue/client'
|
||||
import { defineNuxtPlugin } from '#app/nuxt'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import unheadPlugins from '#build/unhead-plugins.mjs'
|
||||
import unheadOptions from '#build/unhead-options.mjs'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt:head',
|
||||
@ -11,14 +10,7 @@ export default defineNuxtPlugin({
|
||||
setup (nuxtApp) {
|
||||
const head = import.meta.server
|
||||
? nuxtApp.ssrContext!.head
|
||||
: createClientHead({
|
||||
plugins: unheadPlugins,
|
||||
})
|
||||
// allow useHead to be used outside a Vue context but within a Nuxt context
|
||||
setHeadInjectionHandler(
|
||||
// need a fresh instance of the nuxt app to avoid parallel requests interfering with each other
|
||||
() => useNuxtApp().vueApp._context.provides.usehead,
|
||||
)
|
||||
: createClientHead(unheadOptions)
|
||||
// nuxt.config appHead is set server-side within the renderer
|
||||
nuxtApp.vueApp.use(head)
|
||||
|
||||
|
@ -105,6 +105,10 @@ const granularAppPresets: InlinePreset[] = [
|
||||
imports: ['useRuntimeHook'],
|
||||
from: '#app/composables/runtime-hook',
|
||||
},
|
||||
{
|
||||
imports: ['useHead', 'useHeadSafe', 'useServerHeadSafe', 'useServerHead', 'useSeoMeta', 'useServerSeoMeta', 'injectHead'],
|
||||
from: '#app/composables/head',
|
||||
},
|
||||
]
|
||||
|
||||
export const scriptsStubsPreset = {
|
||||
|
208
packages/nuxt/test/unhead-imports.test.ts
Normal file
208
packages/nuxt/test/unhead-imports.test.ts
Normal file
@ -0,0 +1,208 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||
import { UnheadImportsPlugin } from '../src/head/plugins/unhead-imports'
|
||||
|
||||
describe('UnheadImportsPlugin', () => {
|
||||
// Helper function to transform code
|
||||
function transform (code: string, id = 'app.vue') {
|
||||
const plugin = UnheadImportsPlugin({ rootDir: import.meta.dirname, sourcemap: false }).raw({}, {} as any) as any
|
||||
return plugin.transformInclude(id) ? Promise.resolve(plugin.transform(code, id)).then((r: any) => r?.code.replace(/^ {6}/gm, '').trim()) : null
|
||||
}
|
||||
|
||||
describe('transformInclude', () => {
|
||||
// @ts-expect-error untyped
|
||||
const transformInclude = UnheadImportsPlugin({ rootDir: process.cwd(), sourcemap: false }).raw({}, {} as any).transformInclude
|
||||
|
||||
it('should include JS files', () => {
|
||||
expect(transformInclude('/project/components/MyComponent.js')).toBe(true)
|
||||
})
|
||||
|
||||
it('should include TypeScript files', () => {
|
||||
expect(transformInclude('/project/components/MyComponent.ts')).toBe(true)
|
||||
})
|
||||
|
||||
it('should include Vue files', () => {
|
||||
expect(transformInclude('/project/components/MyComponent.vue')).toBe(true)
|
||||
})
|
||||
|
||||
it('should exclude virtual files', () => {
|
||||
expect(transformInclude('virtual:my-plugin')).toBe(false)
|
||||
})
|
||||
|
||||
it('should exclude files from unhead libraries', () => {
|
||||
expect(transformInclude('/project/node_modules/@unhead/vue/index.js')).toBe(false)
|
||||
expect(transformInclude('/project/node_modules/unhead/index.js')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transform', () => {
|
||||
it('should not transform code that does not include @unhead/vue', async () => {
|
||||
const code = `import { renderSSRHead } from '@unhead/ssr'`
|
||||
const result = await transform(code, '/project/components/MyComponent.vue')
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should transform imports from @unhead/vue in .vue files', async () => {
|
||||
const sfc = `
|
||||
<script lang="ts" setup>
|
||||
import { useHead } from '@unhead/vue'
|
||||
useHead({ title: 'My Page' })
|
||||
</script>
|
||||
`
|
||||
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
|
||||
|
||||
const result = await transform(res.content, '/project/components/MyComponent.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead } from "@unhead/vue"')
|
||||
})
|
||||
|
||||
it('should transform imports from @unhead/vue in JS files', async () => {
|
||||
const code = `import { useHead } from '@unhead/vue'`
|
||||
const result = await transform(code, '/project/composables/head.ts')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead } from "@unhead/vue"')
|
||||
})
|
||||
|
||||
it('should handle mixed imports correctly', async () => {
|
||||
const code = `
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { useSeoMeta } from '#app/composables/head'
|
||||
`
|
||||
const result = await transform(code, '/project/components/MyComponent.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead, useSeoMeta } from "#app/composables/head"')
|
||||
// Since we're not mocking the AST parsing, we need to rely on the actual behavior
|
||||
// of the plugin for handling imports from #app/composables/head
|
||||
})
|
||||
|
||||
it('should handle renamed imports correctly', async () => {
|
||||
const code = `import { useHead as useHeadAlias } from '@unhead/vue'`
|
||||
const result = await transform(code, '/project/components/MyComponent.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead as useHeadAlias } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead as useHeadAlias } from "@unhead/vue"')
|
||||
})
|
||||
|
||||
it('should handle multiple imports correctly', async () => {
|
||||
const code = `import { useHead, useSeoMeta } from '@unhead/vue'`
|
||||
const result = await transform(code, '/project/components/MyComponent.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead, useSeoMeta } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead, useSeoMeta } from "@unhead/vue"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration tests', () => {
|
||||
it('should handle a Vue component correctly', async () => {
|
||||
const sfc = `
|
||||
<template>
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
const title = ref('Hello World')
|
||||
|
||||
useHead({
|
||||
title: 'My Page'
|
||||
})
|
||||
</script>
|
||||
`
|
||||
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
|
||||
|
||||
const result = await transform(res.content, '/project/components/MyComponent.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead } from "@unhead/vue"')
|
||||
})
|
||||
|
||||
it('should handle a Nuxt plugin correctly', async () => {
|
||||
const code = `
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
useHead({
|
||||
titleTemplate: '%s - My Site'
|
||||
})
|
||||
})
|
||||
`
|
||||
|
||||
const result = await transform(code, '/project/plugins/head.ts')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead } from "@unhead/vue"')
|
||||
})
|
||||
|
||||
it('should handle TypeScript file in a nested directory correctly', async () => {
|
||||
const code = `
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
export function setupHead() {
|
||||
useHead({
|
||||
meta: [
|
||||
{ name: 'description', content: 'My website description' }
|
||||
]
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
const result = await transform(code, '/project/utils/head/setup.ts')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead } from "@unhead/vue"')
|
||||
})
|
||||
|
||||
it('should handle multiple imports from different sources', async () => {
|
||||
const code = `
|
||||
import { useHead, useSeoMeta } from '@unhead/vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
`
|
||||
|
||||
const result = await transform(code, '/project/pages/index.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead, useSeoMeta } from "#app/composables/head"')
|
||||
expect(result).not.toContain('@unhead/vue')
|
||||
})
|
||||
|
||||
it('should handle imports across multiple lines', async () => {
|
||||
const code = `
|
||||
import {
|
||||
useHead,
|
||||
useSeoMeta
|
||||
} from '@unhead/vue'
|
||||
`
|
||||
|
||||
const result = await transform(code, '/project/components/Header.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
// The actual behavior will depend on how the AST parser handles multi-line imports
|
||||
// This test will verify the behavior is consistent
|
||||
})
|
||||
|
||||
it('should handle different quote styles in imports', async () => {
|
||||
const code = `import { useHead } from "@unhead/vue"`
|
||||
const result = await transform(code, '/project/components/MyComponent.vue')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toContain('import { useHead } from "#app/composables/head"')
|
||||
expect(result).not.toContain('import { useHead } from "@unhead/vue"')
|
||||
})
|
||||
})
|
||||
})
|
@ -28,7 +28,7 @@ export default defineBuildConfig({
|
||||
},
|
||||
externals: [
|
||||
// Type imports
|
||||
'@unhead/schema',
|
||||
'@unhead/vue',
|
||||
'@vitejs/plugin-vue',
|
||||
'chokidar',
|
||||
'@vitejs/plugin-vue-jsx',
|
||||
|
@ -39,7 +39,7 @@
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/webpack-bundle-analyzer": "4.7.0",
|
||||
"@types/webpack-hot-middleware": "2.25.9",
|
||||
"@unhead/schema": "1.11.20",
|
||||
"@unhead/vue": "2.0.0-rc.1",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "4.1.1",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
|
@ -260,7 +260,7 @@ export default defineResolvers({
|
||||
|
||||
/**
|
||||
* Customize Nuxt root element id.
|
||||
* @type {typeof import('@unhead/schema').HtmlAttributes}
|
||||
* @type {typeof import('../src/types/head').SerializableHtmlAttributes}
|
||||
*/
|
||||
rootAttrs: {
|
||||
$resolve: async (val, get) => {
|
||||
@ -290,7 +290,7 @@ export default defineResolvers({
|
||||
|
||||
/**
|
||||
* Customize Nuxt Teleport element attributes.
|
||||
* @type {typeof import('@unhead/schema').HtmlAttributes}
|
||||
* @type {typeof import('../src/types/head').SerializableHtmlAttributes}
|
||||
*/
|
||||
teleportAttrs: {
|
||||
$resolve: async (val, get) => {
|
||||
@ -311,7 +311,7 @@ export default defineResolvers({
|
||||
|
||||
/**
|
||||
* Customize Nuxt Nuxt SpaLoader element attributes.
|
||||
* @type {Partial<typeof import('@unhead/schema').HtmlAttributes>}
|
||||
* @type {typeof import('../src/types/head').SerializableHtmlAttributes}
|
||||
*/
|
||||
spaLoaderAttrs: {
|
||||
id: '__nuxt-loader',
|
||||
@ -454,11 +454,27 @@ export default defineResolvers({
|
||||
* An object that allows us to configure the `unhead` nuxt module.
|
||||
*/
|
||||
unhead: {
|
||||
/***
|
||||
* Enable the legacy compatibility mode for `unhead` module. This applies the following changes:
|
||||
* - Disables Capo.js sorting
|
||||
* - Adds the `DeprecationsPlugin`: supports `hid`, `vmid`, `children`, `body`
|
||||
* - Adds the `PromisesPlugin`: supports promises as input
|
||||
*
|
||||
* @see [`unhead` migration documentation](https://unhead-unjs-io.nuxt.dev/docs/migration)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export default defineNuxtConfig({
|
||||
* unhead: {
|
||||
* legacy: true
|
||||
* })
|
||||
* ```
|
||||
* @type {boolean}
|
||||
*/
|
||||
legacy: false,
|
||||
/**
|
||||
* An object that will be passed to `renderSSRHead` to customize the output.
|
||||
*
|
||||
* @see [`unhead` options documentation](https://unhead.unjs.io/setup/ssr/installation#options)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export default defineNuxtConfig({
|
||||
@ -468,7 +484,7 @@ export default defineResolvers({
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
* @type {typeof import('@unhead/schema').RenderSSRHeadOptions}
|
||||
* @type {typeof import('@unhead/vue/types').RenderSSRHeadOptions}
|
||||
*/
|
||||
renderSSRHeadOptions: {
|
||||
$resolve: async (val, get) => {
|
||||
|
@ -4,7 +4,7 @@ export type { Component, ComponentMeta, ComponentsDir, ComponentsOptions, ScanDi
|
||||
export type { AppConfig, AppConfigInput, CustomAppConfig, NuxtAppConfig, NuxtBuilder, NuxtConfig, NuxtConfigLayer, NuxtOptions, PublicRuntimeConfig, RuntimeConfig, RuntimeValue, SchemaDefinition, UpperSnakeCase, ViteConfig } from './types/config'
|
||||
export type { GenerateAppOptions, HookResult, ImportPresetWithDeprecation, NuxtAnalyzeMeta, NuxtHookName, NuxtHooks, NuxtLayout, NuxtMiddleware, NuxtPage, TSReference, VueTSConfig, WatchEvent } from './types/hooks'
|
||||
export type { ImportsOptions } from './types/imports'
|
||||
export type { AppHeadMetaObject, MetaObject, MetaObjectRaw, HeadAugmentations } from './types/head'
|
||||
export type { AppHeadMetaObject, MetaObject, MetaObjectRaw } from './types/head'
|
||||
export type { ModuleDefinition, ModuleMeta, ModuleOptions, ModuleSetupInstallResult, ModuleSetupReturn, NuxtModule, ResolvedModuleOptions } from './types/module'
|
||||
export type { Nuxt, NuxtApp, NuxtPlugin, NuxtPluginTemplate, NuxtTemplate, NuxtTypeTemplate, NuxtServerTemplate, ResolvedNuxtTemplate } from './types/nuxt'
|
||||
export type { RouterConfig, RouterConfigSerializable, RouterOptions } from './types/router'
|
||||
|
@ -1,27 +1,6 @@
|
||||
import type { Head, MergeHead } from '@unhead/schema'
|
||||
import type { AriaAttributes, DataKeys, GlobalAttributes, SerializableHead } from '@unhead/vue/types'
|
||||
|
||||
/** @deprecated Extend types from `@unhead/schema` directly. This may be removed in a future minor version. */
|
||||
export interface HeadAugmentations extends MergeHead {
|
||||
// runtime type modifications
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
base?: {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
link?: {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
meta?: {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
style?: {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
script?: {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
noscript?: {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
htmlAttrs?: {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
bodyAttrs?: {}
|
||||
}
|
||||
|
||||
export type MetaObjectRaw = Head<HeadAugmentations>
|
||||
export type MetaObjectRaw = SerializableHead
|
||||
export type MetaObject = MetaObjectRaw
|
||||
|
||||
export type AppHeadMetaObject = MetaObjectRaw & {
|
||||
@ -37,3 +16,5 @@ export type AppHeadMetaObject = MetaObjectRaw & {
|
||||
*/
|
||||
viewport?: string
|
||||
}
|
||||
|
||||
export type SerializableHtmlAttributes = GlobalAttributes & AriaAttributes & DataKeys
|
||||
|
@ -122,7 +122,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
||||
if (Array.isArray(serverConfig.ssr!.external)) {
|
||||
serverConfig.ssr!.external.push(
|
||||
// explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build
|
||||
'unhead', '@unhead/ssr', 'unctx', 'h3', 'devalue', '@nuxt/devalue', 'radix3', 'rou3', 'unstorage', 'hookable',
|
||||
'unhead', '@unhead/vue', 'unctx', 'h3', 'devalue', '@nuxt/devalue', 'radix3', 'rou3', 'unstorage', 'hookable',
|
||||
// ensure we only have one version of vue if nitro is going to inline anyway
|
||||
...((ctx.nuxt as any)._nitro as Nitro).options.inlineDynamicImports ? ['vue', '@vue/server-renderer', '@unhead/vue'] : [],
|
||||
// dependencies we might share with nitro - these can be inlined (if necessary) in the nitro build
|
||||
|
105
pnpm-lock.yaml
105
pnpm-lock.yaml
@ -14,11 +14,7 @@ overrides:
|
||||
'@nuxt/vite-builder': workspace:*
|
||||
'@nuxt/webpack-builder': workspace:*
|
||||
'@types/node': 22.13.9
|
||||
'@unhead/dom': 1.11.20
|
||||
'@unhead/schema': 1.11.20
|
||||
'@unhead/shared': 1.11.20
|
||||
'@unhead/ssr': 1.11.20
|
||||
'@unhead/vue': 1.11.20
|
||||
'@unhead/vue': 2.0.0-rc.1
|
||||
'@vue/compiler-core': 3.5.13
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
@ -34,7 +30,6 @@ overrides:
|
||||
typescript: 5.8.2
|
||||
ufo: 1.5.4
|
||||
unbuild: 3.5.0
|
||||
unhead: 1.11.20
|
||||
unimport: 4.1.2
|
||||
vite: 6.2.0
|
||||
vue: 3.5.13
|
||||
@ -88,12 +83,9 @@ importers:
|
||||
'@types/semver':
|
||||
specifier: 7.5.8
|
||||
version: 7.5.8
|
||||
'@unhead/schema':
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20
|
||||
'@unhead/vue':
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20(vue@3.5.13(typescript@5.8.2))
|
||||
specifier: 2.0.0-rc.1
|
||||
version: 2.0.0-rc.1(vue@3.5.13(typescript@5.8.2))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 3.0.7
|
||||
version: 3.0.7(vitest@3.0.7(@types/debug@4.1.12)(@types/node@22.13.9)(happy-dom@17.1.9)(jiti@2.4.2)(terser@5.32.0)(tsx@4.19.2)(yaml@2.7.0))
|
||||
@ -347,18 +339,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: 22.13.9
|
||||
version: 22.13.9
|
||||
'@unhead/dom':
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20
|
||||
'@unhead/shared':
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20
|
||||
'@unhead/ssr':
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20
|
||||
'@unhead/vue':
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20(vue@3.5.13(typescript@5.8.2))
|
||||
specifier: 2.0.0-rc.1
|
||||
version: 2.0.0-rc.1(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue/shared':
|
||||
specifier: 3.5.13
|
||||
version: 3.5.13
|
||||
@ -494,9 +477,6 @@ importers:
|
||||
unenv:
|
||||
specifier: ^1.10.0
|
||||
version: 1.10.0
|
||||
unhead:
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20
|
||||
unimport:
|
||||
specifier: 4.1.2
|
||||
version: 4.1.2
|
||||
@ -527,7 +507,7 @@ importers:
|
||||
devDependencies:
|
||||
'@nuxt/scripts':
|
||||
specifier: 0.10.5
|
||||
version: 0.10.5(@types/google.maps@3.58.1)(@types/vimeo__player@2.18.3)(@types/youtube@0.1.0)(@unhead/vue@1.11.20(vue@3.5.13(typescript@5.8.2)))(typescript@5.8.2)
|
||||
version: 0.10.5(@types/google.maps@3.58.1)(@types/vimeo__player@2.18.3)(@types/youtube@0.1.0)(@unhead/vue@2.0.0-rc.1(vue@3.5.13(typescript@5.8.2)))(typescript@5.8.2)
|
||||
'@parcel/watcher':
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
@ -719,9 +699,9 @@ importers:
|
||||
'@types/webpack-hot-middleware':
|
||||
specifier: 2.25.9
|
||||
version: 2.25.9(esbuild@0.25.0)
|
||||
'@unhead/schema':
|
||||
specifier: 1.11.20
|
||||
version: 1.11.20
|
||||
'@unhead/vue':
|
||||
specifier: 2.0.0-rc.1
|
||||
version: 2.0.0-rc.1(vue@3.5.13(typescript@5.8.2))
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 5.2.1
|
||||
version: 5.2.1(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(terser@5.32.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
@ -2145,7 +2125,7 @@ packages:
|
||||
'@types/google.maps': ^3.58.1
|
||||
'@types/vimeo__player': ^2.18.3
|
||||
'@types/youtube': ^0.1.0
|
||||
'@unhead/vue': 1.11.20
|
||||
'@unhead/vue': 2.0.0-rc.1
|
||||
peerDependenciesMeta:
|
||||
'@stripe/stripe-js':
|
||||
optional: true
|
||||
@ -2952,20 +2932,8 @@ packages:
|
||||
'@ungap/structured-clone@1.2.0':
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
|
||||
'@unhead/dom@1.11.20':
|
||||
resolution: {integrity: sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==}
|
||||
|
||||
'@unhead/schema@1.11.20':
|
||||
resolution: {integrity: sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==}
|
||||
|
||||
'@unhead/shared@1.11.20':
|
||||
resolution: {integrity: sha512-1MOrBkGgkUXS+sOKz/DBh4U20DNoITlJwpmvSInxEUNhghSNb56S0RnaHRq0iHkhrO/cDgz2zvfdlRpoPLGI3w==}
|
||||
|
||||
'@unhead/ssr@1.11.20':
|
||||
resolution: {integrity: sha512-j6ehzmdWGAvv0TEZyLE3WBnG1ULnsbKQcLqBDh3fvKS6b3xutcVZB7mjvrVE7ckSZt6WwOtG0ED3NJDS7IjzBA==}
|
||||
|
||||
'@unhead/vue@1.11.20':
|
||||
resolution: {integrity: sha512-sqQaLbwqY9TvLEGeq8Fd7+F2TIuV3nZ5ihVISHjWpAM3y7DwNWRU7NmT9+yYT+2/jw1Vjwdkv5/HvDnvCLrgmg==}
|
||||
'@unhead/vue@2.0.0-rc.1':
|
||||
resolution: {integrity: sha512-Y9DQ8gwAVXSkVJ76sMZsfdfVmXaWCvz3viJMQN3WnZ1DcWknme5HoFgBEDHyYxIZymP8dHsxWAFk1p+yRZE4Bw==}
|
||||
peerDependencies:
|
||||
vue: 3.5.13
|
||||
|
||||
@ -5993,9 +5961,6 @@ packages:
|
||||
package-manager-detector@0.2.9:
|
||||
resolution: {integrity: sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==}
|
||||
|
||||
packrup@0.1.2:
|
||||
resolution: {integrity: sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@ -7301,8 +7266,8 @@ packages:
|
||||
unenv@1.10.0:
|
||||
resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==}
|
||||
|
||||
unhead@1.11.20:
|
||||
resolution: {integrity: sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==}
|
||||
unhead@2.0.0-rc.1:
|
||||
resolution: {integrity: sha512-jy/rBmC8Q+9EvSpkMYL4gvozSJGe7XTPPcC6NCzh8dUhNxC5eiwtIYdS/gyxvgOnItb9e+B/fHvrFFgLUkwuzQ==}
|
||||
|
||||
unicode-emoji-modifier-base@1.0.0:
|
||||
resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
|
||||
@ -7918,9 +7883,6 @@ packages:
|
||||
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zhead@2.2.4:
|
||||
resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==}
|
||||
|
||||
zip-stream@6.0.1:
|
||||
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
||||
engines: {node: '>= 14'}
|
||||
@ -8835,10 +8797,10 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
webpack: 5.98.0
|
||||
|
||||
'@nuxt/scripts@0.10.5(@types/google.maps@3.58.1)(@types/vimeo__player@2.18.3)(@types/youtube@0.1.0)(@unhead/vue@1.11.20(vue@3.5.13(typescript@5.8.2)))(typescript@5.8.2)':
|
||||
'@nuxt/scripts@0.10.5(@types/google.maps@3.58.1)(@types/vimeo__player@2.18.3)(@types/youtube@0.1.0)(@unhead/vue@2.0.0-rc.1(vue@3.5.13(typescript@5.8.2)))(typescript@5.8.2)':
|
||||
dependencies:
|
||||
'@nuxt/kit': link:packages/kit
|
||||
'@unhead/vue': 1.11.20(vue@3.5.13(typescript@5.8.2))
|
||||
'@unhead/vue': 2.0.0-rc.1(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/core': 12.7.0(typescript@5.8.2)
|
||||
consola: 3.4.0
|
||||
defu: 6.1.4
|
||||
@ -9790,32 +9752,10 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
|
||||
'@unhead/dom@1.11.20':
|
||||
dependencies:
|
||||
'@unhead/schema': 1.11.20
|
||||
'@unhead/shared': 1.11.20
|
||||
|
||||
'@unhead/schema@1.11.20':
|
||||
'@unhead/vue@2.0.0-rc.1(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
hookable: 5.5.3
|
||||
zhead: 2.2.4
|
||||
|
||||
'@unhead/shared@1.11.20':
|
||||
dependencies:
|
||||
'@unhead/schema': 1.11.20
|
||||
packrup: 0.1.2
|
||||
|
||||
'@unhead/ssr@1.11.20':
|
||||
dependencies:
|
||||
'@unhead/schema': 1.11.20
|
||||
'@unhead/shared': 1.11.20
|
||||
|
||||
'@unhead/vue@1.11.20(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@unhead/schema': 1.11.20
|
||||
'@unhead/shared': 1.11.20
|
||||
hookable: 5.5.3
|
||||
unhead: 1.11.20
|
||||
unhead: 2.0.0-rc.1
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
||||
'@unocss/astro@66.0.0(vite@6.2.0(@types/node@22.13.9)(jiti@2.4.2)(sass@1.78.0)(terser@5.32.0)(tsx@4.19.2)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
@ -13530,8 +13470,6 @@ snapshots:
|
||||
|
||||
package-manager-detector@0.2.9: {}
|
||||
|
||||
packrup@0.1.2: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@ -14943,11 +14881,8 @@ snapshots:
|
||||
node-fetch-native: 1.6.6
|
||||
pathe: 1.1.2
|
||||
|
||||
unhead@1.11.20:
|
||||
unhead@2.0.0-rc.1:
|
||||
dependencies:
|
||||
'@unhead/dom': 1.11.20
|
||||
'@unhead/schema': 1.11.20
|
||||
'@unhead/shared': 1.11.20
|
||||
hookable: 5.5.3
|
||||
|
||||
unicode-emoji-modifier-base@1.0.0: {}
|
||||
@ -15687,8 +15622,6 @@ snapshots:
|
||||
|
||||
yoctocolors@2.1.1: {}
|
||||
|
||||
zhead@2.2.4: {}
|
||||
|
||||
zip-stream@6.0.1:
|
||||
dependencies:
|
||||
archiver-utils: 5.0.2
|
||||
|
@ -7,7 +7,6 @@ import { join, normalize } from 'pathe'
|
||||
import { $fetch as _$fetch, createPage, fetch, isDev, setup, startServer, url, useTestContext } from '@nuxt/test-utils/e2e'
|
||||
import { $fetchComponent } from '@nuxt/test-utils/experimental'
|
||||
|
||||
import { resolveUnrefHeadInput } from '@unhead/vue'
|
||||
import { expectNoClientErrors, expectWithPolling, gotoPath, isRenderingJson, parseData, parsePayload, renderPage } from './utils'
|
||||
|
||||
import type { NuxtIslandResponse } from '#app'
|
||||
@ -1018,13 +1017,15 @@ 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"></script></body>')
|
||||
|
||||
const bodyHtml = headHtml.match(/<body[^>]*>(.*)<\/body>/s)![1]
|
||||
expect(bodyHtml).toContain('<script src="https://a-body-appended-script.com"></script>')
|
||||
|
||||
const indexHtml = await $fetch<string>('/')
|
||||
// should render charset by default
|
||||
expect(indexHtml).toContain('<meta charset="utf-8">')
|
||||
// should render <Head> components
|
||||
expect(indexHtml).toContain('<title>Basic fixture</title>')
|
||||
expect(indexHtml).toContain('<title>Basic fixture - Fixture</title>')
|
||||
})
|
||||
|
||||
it('SSR script setup should render tags', async () => {
|
||||
@ -1039,7 +1040,7 @@ describe('head tags', () => {
|
||||
// useServerHead - shorthands
|
||||
expect(headHtml).toContain('>/* Custom styles */</style>')
|
||||
// useHeadSafe - removes dangerous content
|
||||
expect(headHtml).toContain('<script id="xss-script"></script>')
|
||||
expect(headHtml).not.toContain('<script id="xss-script">')
|
||||
expect(headHtml).toContain('<meta content="0;javascript:alert(1)">')
|
||||
})
|
||||
|
||||
@ -2258,6 +2259,7 @@ describe('component islands', () => {
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
"titleTemplate": "%s - Fixture",
|
||||
},
|
||||
"html": "<pre data-island-uid> Route: /foo
|
||||
</pre>",
|
||||
@ -2280,6 +2282,7 @@ describe('component islands', () => {
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
"titleTemplate": "%s - Fixture",
|
||||
},
|
||||
"html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"><!--teleport start--><!--teleport end--></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--></div>",
|
||||
"slots": {
|
||||
@ -2343,6 +2346,7 @@ describe('component islands', () => {
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
"titleTemplate": "%s - Fixture",
|
||||
},
|
||||
"html": "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div>",
|
||||
"props": {},
|
||||
@ -2374,6 +2378,7 @@ describe('component islands', () => {
|
||||
"head": {
|
||||
"link": [],
|
||||
"style": [],
|
||||
"titleTemplate": "%s - Fixture",
|
||||
},
|
||||
"html": "<div data-island-uid> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component below is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-uid data-island-component></div><!--teleport start--><!--teleport end--><!--]--></div></div>",
|
||||
"slots": {},
|
||||
@ -2405,7 +2410,7 @@ describe('component islands', () => {
|
||||
for (const key in result.head) {
|
||||
if (key === 'link') {
|
||||
result.head[key] = result.head[key]?.map((h) => {
|
||||
h.href &&= resolveUnrefHeadInput(h.href).replace(fixtureDir, '/<rootDir>').replaceAll('//', '/')
|
||||
h.href &&= (h.href).replace(fixtureDir, '/<rootDir>').replaceAll('//', '/')
|
||||
return h
|
||||
})
|
||||
}
|
||||
@ -2422,6 +2427,7 @@ describe('component islands', () => {
|
||||
"innerHTML": "pre[data-v-xxxxx]{color:#00f}",
|
||||
},
|
||||
],
|
||||
"titleTemplate": "%s - Fixture",
|
||||
}
|
||||
`)
|
||||
} else if (isDev() && !isWebpack) {
|
||||
@ -2439,6 +2445,7 @@ describe('component islands', () => {
|
||||
},
|
||||
],
|
||||
"style": [],
|
||||
"titleTemplate": "%s - Fixture",
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
@ -23,8 +23,8 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const [clientStats, clientStatsInlined] = await Promise.all((['.output', '.output-inline'])
|
||||
.map(outputDir => analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public'))))
|
||||
|
||||
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"115k"`)
|
||||
expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"115k"`)
|
||||
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"112k"`)
|
||||
expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"112k"`)
|
||||
|
||||
const files = new Set([...clientStats!.files, ...clientStatsInlined!.files].map(f => f.replace(/\..*\.js/, '.js')))
|
||||
|
||||
@ -38,7 +38,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
it('default client bundle size (pages)', async () => {
|
||||
const clientStats = await analyzeSizes(['**/*.js'], join(pagesRootDir, '.output/public'))
|
||||
|
||||
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"174k"`)
|
||||
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"171k"`)
|
||||
|
||||
const files = clientStats!.files.map(f => f.replace(/\..*\.js/, '.js'))
|
||||
|
||||
@ -58,10 +58,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"208k"`)
|
||||
|
||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1411k"`)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1397k"`)
|
||||
|
||||
const packages = modules.files
|
||||
.filter(m => m.endsWith('package.json'))
|
||||
@ -70,9 +70,6 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
expect(packages).toMatchInlineSnapshot(`
|
||||
[
|
||||
"@babel/parser",
|
||||
"@unhead/dom",
|
||||
"@unhead/shared",
|
||||
"@unhead/ssr",
|
||||
"@vue/compiler-core",
|
||||
"@vue/compiler-dom",
|
||||
"@vue/compiler-ssr",
|
||||
@ -87,7 +84,6 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
"estree-walker",
|
||||
"hookable",
|
||||
"node-mock-http",
|
||||
"packrup",
|
||||
"source-map-js",
|
||||
"ufo",
|
||||
"unhead",
|
||||
@ -101,10 +97,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(rootDir, '.output-inline/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"560k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"558k"`)
|
||||
|
||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"105k"`)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"90.9k"`)
|
||||
|
||||
const packages = modules.files
|
||||
.filter(m => m.endsWith('package.json'))
|
||||
@ -112,14 +108,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
.sort()
|
||||
expect(packages).toMatchInlineSnapshot(`
|
||||
[
|
||||
"@unhead/dom",
|
||||
"@unhead/shared",
|
||||
"@unhead/ssr",
|
||||
"db0",
|
||||
"devalue",
|
||||
"hookable",
|
||||
"node-mock-http",
|
||||
"packrup",
|
||||
"unhead",
|
||||
]
|
||||
`)
|
||||
@ -129,10 +121,10 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const serverDir = join(pagesRootDir, '.output/server')
|
||||
|
||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"304k"`)
|
||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`)
|
||||
|
||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1411k"`)
|
||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1408k"`)
|
||||
|
||||
const packages = modules.files
|
||||
.filter(m => m.endsWith('package.json'))
|
||||
@ -141,9 +133,6 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
expect(packages).toMatchInlineSnapshot(`
|
||||
[
|
||||
"@babel/parser",
|
||||
"@unhead/dom",
|
||||
"@unhead/shared",
|
||||
"@unhead/ssr",
|
||||
"@vue/compiler-core",
|
||||
"@vue/compiler-dom",
|
||||
"@vue/compiler-ssr",
|
||||
@ -158,7 +147,6 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
"estree-walker",
|
||||
"hookable",
|
||||
"node-mock-http",
|
||||
"packrup",
|
||||
"source-map-js",
|
||||
"ufo",
|
||||
"unhead",
|
||||
|
@ -1,4 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { TemplateParamsPlugin } from '@unhead/vue/plugins'
|
||||
// Unhead v2 requires an opt-in to template params
|
||||
const head = injectHead()
|
||||
head.use(TemplateParamsPlugin)
|
||||
|
||||
const description = ref('head script setup description for %site.name')
|
||||
const siteName = ref()
|
||||
// server meta
|
||||
|
2
test/fixtures/basic/pages/head.vue
vendored
2
test/fixtures/basic/pages/head.vue
vendored
@ -18,7 +18,7 @@ export default defineNuxtComponent({
|
||||
script: [
|
||||
{
|
||||
src: 'https://a-body-appended-script.com',
|
||||
body: true,
|
||||
tagPosition: 'bodyClose',
|
||||
},
|
||||
],
|
||||
meta: [{ name: 'description', content: 'first' }],
|
||||
|
@ -76,6 +76,7 @@ describe('composables', () => {
|
||||
'getAppManifest',
|
||||
'useHydration',
|
||||
'getRouteRules',
|
||||
'injectHead',
|
||||
'onNuxtReady',
|
||||
'callOnce',
|
||||
'setResponseStatus',
|
||||
@ -114,10 +115,13 @@ describe('composables', () => {
|
||||
'useId',
|
||||
'useFetch',
|
||||
'useHead',
|
||||
'useHeadSafe',
|
||||
'useLazyFetch',
|
||||
'useLazyAsyncData',
|
||||
'useRouter',
|
||||
'useSeoMeta',
|
||||
'useServerHead',
|
||||
'useServerHeadSafe',
|
||||
'useServerSeoMeta',
|
||||
'usePreviewMode',
|
||||
]
|
||||
|
@ -32,6 +32,9 @@
|
||||
],
|
||||
"#app/*": [
|
||||
"./packages/nuxt/src/app/*"
|
||||
],
|
||||
"#unhead/composables": [
|
||||
"./packages/nuxt/src/head/runtime/composables/v4"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user