Skip to content

Commit b67d6dd

Browse files
committedFeb 27, 2025·
fix: SerializableHead input support and exports
1 parent 49ec415 commit b67d6dd

File tree

8 files changed

+83
-28
lines changed

8 files changed

+83
-28
lines changed
 

‎packages/unhead/src/composables.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@ import type {
22
ActiveHeadEntry,
33
HeadEntryOptions,
44
HeadSafe,
5+
ResolvableHead,
6+
SerializableHead,
57
Unhead,
68
UseSeoMetaInput,
79
} from './types'
810
import { FlatMetaPlugin } from './plugins/flatMeta'
911
import { SafeInputPlugin } from './plugins/safe'
1012

11-
export function useHead<T extends Unhead<any>>(unhead: T, input: Parameters<T['push']>[0] = {}, options: HeadEntryOptions = {}): ReturnType<T['push']> {
12-
return unhead.push(input, options) as ReturnType<T['push']>
13+
export function useHead<T extends Unhead<any>>(unhead: T, input: ResolvableHead | SerializableHead = {}, options: HeadEntryOptions = {}): ActiveHeadEntry<ResolvableHead | SerializableHead> {
14+
return unhead.push(input, options) as ActiveHeadEntry<ResolvableHead | SerializableHead>
1315
}
1416

1517
export function useHeadSafe<T extends Unhead<any>>(unhead: T, input: HeadSafe = {}, options: HeadEntryOptions = {}): ActiveHeadEntry<HeadSafe> {
1618
unhead.use(SafeInputPlugin)
17-
return useHead(unhead, input, Object.assign(options, { _safe: true }))
19+
// @ts-expect-error untyped
20+
return useHead(unhead, input, Object.assign(options, { _safe: true })) as ActiveHeadEntry<HeadSafe>
1821
}
1922

2023
export function useSeoMeta<T extends Unhead<any>>(unhead: T, input: UseSeoMetaInput = {}, options?: HeadEntryOptions): ActiveHeadEntry<UseSeoMetaInput> {

‎packages/unhead/src/scripts/useScript.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {
2-
ResolvedHead,
2+
SerializableHead,
33
Unhead,
44
} from '../types'
55
import type {
@@ -136,7 +136,7 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
136136
const $url = new URL(src)
137137
href = `${$url.protocol}//${$url.host}`
138138
}
139-
const link: Required<ResolvedHead>['link'][0] = {
139+
const link: Required<SerializableHead>['link'][0] = {
140140
href,
141141
rel,
142142
crossorigin: input.crossorigin || isCrossOrigin ? 'anonymous' : undefined,
@@ -154,7 +154,7 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
154154
script._triggerPromises = [] // clear any pending promises
155155
if (!script.entry) {
156156
syncStatus('loading')
157-
const defaults: Required<ResolvedHead>['script'][0] = {
157+
const defaults: Required<SerializableHead>['script'][0] = {
158158
defer: true,
159159
fetchpriority: 'low',
160160
}

‎packages/unhead/src/server/util/extractUnheadInputFromHtml.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ResolvedHead } from '../../types'
1+
import type { SerializableHead } from '../../types'
22

33
const Attrs = /(\w+)(?:=["']([^"']*)["'])?/g
44
const HtmlTag = /<html[^>]*>/
@@ -9,34 +9,35 @@ const ClosingTags = /<(title|script|style)[^>]*>[\s\S]*?<\/\1>/g
99
// eslint-disable-next-line regexp/no-misleading-capturing-group
1010
const NewLines = /(\n\s*)+/g
1111

12-
function extractAttributes(tag: string) {
12+
function extractAttributes<K extends 'htmlAttrs' | 'bodyAttrs' | 'meta'>(tag: string): SerializableHead[K] {
1313
// inner should be between the < and > (non greedy), split on ' ' and after index 0
1414
const inner = tag.match(/<([^>]*)>/)?.[1].split(' ').slice(1).join(' ')
1515
if (!inner)
16-
return {}
16+
return {} as SerializableHead[K]
1717
const attrs = inner.match(Attrs)
18-
return attrs?.reduce((acc, attr) => {
18+
return (attrs?.reduce((acc, attr) => {
1919
const sep = attr.indexOf('=')
2020
const key = sep > 0 ? attr.slice(0, sep) : attr
2121
const val = sep > 0 ? attr.slice(sep + 1).slice(1, -1) : true
2222
return { ...acc, [key]: val }
23-
}, {}) || {}
23+
}, {}) || {}) as SerializableHead[K]
2424
}
2525

2626
export function extractUnheadInputFromHtml(html: string) {
27-
const input: ResolvedHead<any> = {}
28-
input.htmlAttrs = extractAttributes(html.match(HtmlTag)?.[0] || '')
27+
const input = {} as SerializableHead
28+
input.htmlAttrs = extractAttributes<'htmlAttrs'>(html.match(HtmlTag)?.[0] || '')
2929
html = html.replace(HtmlTag, '<html>')
3030

31-
input.bodyAttrs = extractAttributes(html.match(BodyTag)?.[0] || '')
31+
input.bodyAttrs = extractAttributes<'bodyAttrs'>(html.match(BodyTag)?.[0] || '')
3232
html = html.replace(BodyTag, '<body>')
3333

3434
const innerHead = html.match(HeadContent)?.[1] || ''
3535
innerHead.match(SelfClosingTags)?.forEach((s) => {
3636
html = html.replace(s, '')
3737
const tag = s.split(' ')[0].slice(1) as 'meta'
3838
input[tag] = input[tag] || []
39-
input[tag].push(extractAttributes(s) as any)
39+
// @ts-expect-error untyped
40+
input[tag].push(extractAttributes<'meta'>(s))
4041
})
4142

4243
innerHead.match(ClosingTags)

‎packages/unhead/src/types/head.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Hookable, NestedHooks } from 'hookable'
22
import type { HeadHooks } from './hooks'
33
import type { DomPluginOptions } from './plugins'
4-
import type { ResolvableHead } from './schema'
4+
import type { ResolvableHead, SerializableHead } from './schema'
55
import type { HeadTag, ProcessesTemplateParams, ResolvesDuplicates, TagPosition, TagPriority, TemplateParams } from './tags'
66

77
/**
@@ -110,7 +110,7 @@ export interface CreateHeadOptions {
110110
*
111111
* Any tags here are added with low priority.
112112
*/
113-
init?: (ResolvableHead<any> | undefined)[]
113+
init?: (ResolvableHead<any> | SerializableHead<any> | undefined)[]
114114
/**
115115
* Disable the Capo.js tag sorting algorithm.
116116
*
@@ -158,7 +158,7 @@ export interface HeadEntryOptions extends TagPosition, TagPriority, ProcessesTem
158158
_index?: number
159159
}
160160

161-
export interface Unhead<Input = ResolvableHead> {
161+
export interface Unhead<Input = ResolvableHead | SerializableHead> {
162162
/**
163163
* Registered plugins.
164164
*/

‎packages/unhead/src/types/schema/head.ts

+32-7
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ export interface BodyAttr extends Omit<BaseBodyAttr, 'class' | 'style'> {
5252
*
5353
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class
5454
*/
55-
class?: MaybeArray<string> | Record<string, boolean>
55+
class?: MaybeArray<ResolvableValue<Stringable>> | Record<string, ResolvableValue<boolean>>
5656
/**
5757
* The style attribute contains CSS styling declarations to be applied to the element.
5858
*
5959
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style
6060
*/
61-
style?: MaybeArray<string> | Record<string, ResolvableValue<string>>
61+
style?: MaybeArray<ResolvableValue<Stringable>> | Record<string, ResolvableValue<Stringable>>
6262
}
6363

6464
export interface HtmlAttr extends Omit<_HtmlAttributes, 'class' | 'style'> {
@@ -67,13 +67,13 @@ export interface HtmlAttr extends Omit<_HtmlAttributes, 'class' | 'style'> {
6767
*
6868
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class
6969
*/
70-
class?: MaybeArray<string> | Record<string, boolean>
70+
class?: MaybeArray<ResolvableValue<Stringable>> | Record<string, ResolvableValue<boolean>>
7171
/**
7272
* The style attribute contains CSS styling declarations to be applied to the element.
7373
*
7474
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/style
7575
*/
76-
style?: MaybeArray<string> | Record<string, ResolvableValue<string>>
76+
style?: MaybeArray<ResolvableValue<Stringable>> | Record<string, ResolvableValue<Stringable>>
7777
}
7878

7979
export interface BaseMeta extends Omit<_Meta, 'content'> {
@@ -193,7 +193,7 @@ export interface ResolvableHead<E extends MergeHead = SchemaAugmentations> exten
193193
bodyAttrs?: ResolvableValue<BodyAttributes<E['bodyAttrs']>>
194194
}
195195

196-
export interface ResolvedHead<E extends MergeHead = ResolvedSchemaAugmentations> extends HeadUtils {
196+
export interface NormalizedHead<E extends MergeHead = ResolvedSchemaAugmentations> extends HeadUtils {
197197
title?: ResolvedTitle
198198
base?: ResolvedBase<E['base']>
199199
link?: ResolvedLink<E['link']>[]
@@ -205,9 +205,34 @@ export interface ResolvedHead<E extends MergeHead = ResolvedSchemaAugmentations>
205205
bodyAttrs?: ResolvedBodyAttributes<E['bodyAttrs']>
206206
}
207207

208-
export type Head = ResolvableHead & ResolvedHead
208+
type AsSerializable<S extends keyof ResolvedSchemaAugmentations> = ResolvedSchemaAugmentations[S] & DataKeys
209+
210+
export interface SerializableHead<E extends MergeHead = ResolvedSchemaAugmentations> {
211+
/**
212+
* Generate the title from a template.
213+
*
214+
* Should include a `%s` placeholder for the title, for example `%s - My Site`.
215+
*/
216+
titleTemplate?: string
217+
/**
218+
* Variables used to substitute in the title and meta content.
219+
*/
220+
templateParams?: Record<string, Record<string, string | boolean | number> | string | boolean | number>
221+
title?: string | ({ textContent: string } & ResolvedSchemaAugmentations['title'])
222+
base?: Partial<Merge<ResolvedSchemaAugmentations['base'], _Base>> & DefinedValueOrEmptyObject<E['base']>
223+
link?: (LinkBase & AsSerializable<'link'> & HttpEventAttributes)[]
224+
meta?: (_Meta & AsSerializable<'meta'>)[]
225+
style?: (_Style & AsSerializable<'style'>)[]
226+
script?: (ScriptBase & AsSerializable<'script'> & HttpEventAttributes)[]
227+
noscript?: (_Noscript & AsSerializable<'noscript'>)[]
228+
htmlAttrs?: _HtmlAttributes & AsSerializable<'htmlAttrs'>
229+
bodyAttrs?: BaseBodyAttr & AsSerializable<'bodyAttrs'> & BodyEvents
230+
}
231+
232+
export type Head = SerializableHead
233+
export type ResolvedHead = SerializableHead
209234

210235
export type UseSeoMetaInput = MetaFlatInput & { title?: Title, titleTemplate?: TitleTemplate }
211-
export type UseHeadInput<T extends MergeHead = MergeHead> = ResolvableHead<T>
236+
export type UseHeadInput<T extends MergeHead = MergeHead> = ResolvableHead<T> | SerializableHead<T>
212237

213238
export { type BodyEvents, type DataKeys, type HttpEventAttributes, type LinkBase, type MetaFlatInput, type ScriptBase }

‎packages/unhead/test/unit/types.test.ts

+27
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SerializableHead } from '../../src/types'
12
import { useHead, useHeadSafe, useSeoMeta } from '../../src/composables'
23
import { createHead } from '../../src/server'
34

@@ -56,4 +57,30 @@ describe('types', () => {
5657
},
5758
})
5859
})
60+
it('types SerializableHead', () => {
61+
const head = createHead()
62+
const input: SerializableHead = {
63+
title: 'Hello',
64+
meta: [
65+
{ name: 'description', content: 'Static content' },
66+
{ property: 'og:image', content: 'https://example.com/1.jpg' },
67+
],
68+
script: [
69+
{ src: 'https://example.com/script.js' },
70+
],
71+
link: [
72+
{ rel: 'stylesheet', href: 'style1.css' },
73+
],
74+
// Validate HTML attributes
75+
htmlAttrs: {
76+
lang: 'en',
77+
class: 'dark',
78+
},
79+
// Validate body attributes
80+
bodyAttrs: {
81+
class: 'bg-gray-100',
82+
},
83+
}
84+
useHead(head, input)
85+
})
5986
})

‎packages/vue/src/types/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export type * from './safeSchema'
22
export type * from './schema'
33
export type * from './util'
4-
export type { ActiveHeadEntry, Head, HeadEntryOptions, HeadTag, MergeHead, RenderSSRHeadOptions, ResolvableHead, ResolvedBase, ResolvedBodyAttributes, ResolvedHead, ResolvedHtmlAttributes, ResolvedLink, ResolvedMeta, ResolvedNoscript, ResolvedScript, ResolvedStyle, ResolvedTitle, ResolvedTitleTemplate, Unhead } from 'unhead/types'
4+
export type { ActiveHeadEntry, Head, HeadEntryOptions, HeadTag, MergeHead, NormalizedHead, RenderSSRHeadOptions, ResolvableHead, ResolvedBase, ResolvedBodyAttributes, ResolvedHtmlAttributes, ResolvedLink, ResolvedMeta, ResolvedNoscript, ResolvedScript, ResolvedStyle, ResolvedTitle, ResolvedTitleTemplate, SerializableHead, Unhead } from 'unhead/types'

‎packages/vue/src/utils.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { ResolvedHead } from 'unhead/types'
21
import { walkResolver } from 'unhead/utils'
32
import { VueResolver } from './resolver'
43

@@ -7,7 +6,7 @@ export * from 'unhead/utils'
76
/**
87
* @deprecated Use head.resolveTags() instead
98
*/
10-
export function resolveUnrefHeadInput(input: any): ResolvedHead {
9+
export function resolveUnrefHeadInput<T extends Record<string, any>>(input: T): T {
1110
return walkResolver(input, VueResolver)
1211
}
1312

0 commit comments

Comments
 (0)
Please sign in to comment.