From bc0a3948e2b463e282f086fe8dadd87109c45361 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 5 Mar 2025 22:47:58 +1100 Subject: [PATCH] fix(nuxt): consolidate head component context (#31209) --- docs/1.getting-started/5.seo-meta.md | 11 +- packages/nuxt/src/head/runtime/components.ts | 216 ++++++++++++------- test/basic.test.ts | 2 +- 3 files changed, 145 insertions(+), 84 deletions(-) diff --git a/docs/1.getting-started/5.seo-meta.md b/docs/1.getting-started/5.seo-meta.md index 88febb060f..a876cccd00 100644 --- a/docs/1.getting-started/5.seo-meta.md +++ b/docs/1.getting-started/5.seo-meta.md @@ -100,9 +100,10 @@ useSeoMeta({ ## Components -Nuxt provides ``, `<Base>`, `<NoScript>`, `<Style>`, `<Meta>`, `<Link>`, `<Body>`, `<Html>` and `<Head>` components so that you can interact directly with your metadata within your component's template. +While using [`useHead`](/docs/api/composables/use-head) is recommended in all cases, you may have a personal preference for defining your head tags in your template using components. -Because these component names match native HTML elements, they must be capitalized in the template. +Nuxt provides the following components for this purpose: `<Title>`, `<Base>`, `<NoScript>`, `<Style>`, `<Meta>`, `<Link>`, `<Body>`, `<Html>` and `<Head>`. Note +the capitalization of these components ensuring we don't use invalid native HTML tags. `<Head>` and `<Body>` can accept nested meta tags (for aesthetic reasons) but this does not affect _where_ the nested meta tags are rendered in the final HTML. @@ -118,7 +119,9 @@ const title = ref('Hello World') <Head> <Title>{{ title }} - +

{{ title }}

@@ -126,6 +129,8 @@ const title = ref('Hello World') ``` +It's suggested to wrap your components in either a `` or `` components as tags will be deduped more intuitively. + ## Types Below are the non-reactive types used for [`useHead`](/docs/api/composables/use-head), [`app.head`](/docs/api/nuxt-config#head) and components. diff --git a/packages/nuxt/src/head/runtime/components.ts b/packages/nuxt/src/head/runtime/components.ts index 64ccc0ae03..a3d3e4274b 100644 --- a/packages/nuxt/src/head/runtime/components.ts +++ b/packages/nuxt/src/head/runtime/components.ts @@ -1,30 +1,73 @@ -import { defineComponent } from 'vue' -import type { PropType, SetupContext } from 'vue' +import { defineComponent, inject, onUnmounted, provide, reactive } from 'vue' +import type { PropType } from 'vue' +import type { + BodyAttributes, + HtmlAttributes, + Noscript, + Base as UnheadBase, + Link as UnheadLink, + Meta as UnheadMeta, + Style as UnheadStyle, +} from '@unhead/vue/types' import type { CrossOrigin, FetchPriority, HTTPEquiv, LinkRelationship, - Props, ReferrerPolicy, Target, } from './types' import { useHead } from '#app/composables/head' -const removeUndefinedProps = (props: Props) => { - const filteredProps = Object.create(null) - for (const key in props) { - const value = props[key] - if (value !== undefined) { - filteredProps[key] = value - } - } - return filteredProps +interface HeadComponents { + base?: UnheadBase | null + bodyAttrs?: BodyAttributes | null + htmlAttrs?: HtmlAttributes | null + link?: (UnheadLink | null)[] + meta?: (UnheadMeta | null)[] + noscript?: (Noscript | null)[] + style?: (UnheadStyle | null)[] + title?: string | null +} +type HeadComponentCtx = { input: HeadComponents, entry: ReturnType } +const HeadComponentCtxSymbol = Symbol('head-component') + +const TagPositionProps = { + /** + * @deprecated Use tagPosition + */ + body: { type: Boolean, default: undefined }, + tagPosition: { type: String as PropType }, } -const setupForUseMeta = (metaFactory: (props: Props, ctx: SetupContext) => Record, renderChild?: boolean) => (props: Props, ctx: SetupContext) => { - useHead(() => metaFactory({ ...removeUndefinedProps(props), ...ctx.attrs }, ctx)) - return () => renderChild ? ctx.slots.default?.() : null +const normalizeProps = >(_props: T): Partial => { + const props = Object.fromEntries( + Object.entries(_props).filter(([_, value]) => value !== undefined), + ) as Partial & { tagPosition?: UnheadStyle['tagPosition'], tagPriority: UnheadStyle['tagPriority'] } + if (typeof props.body !== 'undefined') { + props.tagPosition = props.body ? 'bodyClose' : 'head' + } + if (typeof props.renderPriority !== 'undefined') { + props.tagPriority = props.renderPriority + } + return props +} + +function useHeadComponentCtx (): HeadComponentCtx { + return inject(HeadComponentCtxSymbol, createHeadComponentCtx, true) +} + +function createHeadComponentCtx (): HeadComponentCtx { + // avoid creating multiple contexts + const prev = inject(HeadComponentCtxSymbol, null) + if (prev) { + return prev + } + const input = reactive({}) + const entry = useHead(input) + const ctx: HeadComponentCtx = { input, entry } + provide(HeadComponentCtxSymbol, ctx) + return ctx } const globalProps = { @@ -34,7 +77,7 @@ const globalProps = { type: Boolean, default: undefined, }, - class: [String, Object, Array], + class: { type: [String, Object, Array], default: undefined }, contenteditable: { type: Boolean, default: undefined, @@ -67,10 +110,18 @@ const globalProps = { type: Boolean, default: undefined, }, - style: [String, Object, Array], + style: { type: [String, Object, Array], default: undefined }, tabindex: String, title: String, translate: String, + /** + * @deprecated Use tagPriority + */ + renderPriority: [String, Number], + /** + * Unhead prop to modify the priority of the tag. + */ + tagPriority: { type: [String, Number] as PropType }, } //