Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit baaa773

Browse files
authored
fix!: pluggable render() function (#630)
* fix!: sync `renderDOMHead` * doc: missing * fix!: sync `renderSSRHead()` * chore: conflict * fix!: pluggable render functions * fix!: pluggable render functions
1 parent 44d719d commit baaa773

20 files changed

Lines changed: 231 additions & 147 deletions

File tree

packages/angular/client/src/client.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
1-
import type { CreateClientHeadOptions } from 'unhead/types'
1+
import type { CreateClientHeadOptions, Unhead } from 'unhead/types'
22
import { DOCUMENT } from '@angular/common'
33
import { inject, makeEnvironmentProviders } from '@angular/core'
44
import { UnheadInjectionToken } from '@unhead/angular'
5-
import { createHead as _createClientHead, createDebouncedFn, renderDOMHead } from 'unhead/client'
5+
import { createHead as _createClientHead, createDebouncedFn, createDomRenderer } from 'unhead/client'
66

77
export function provideClientHead(options: CreateClientHeadOptions = {}) {
88
return makeEnvironmentProviders([{
99
provide: UnheadInjectionToken,
1010
useFactory: () => {
1111
const document = inject(DOCUMENT)
12-
const head = _createClientHead({
13-
document,
14-
domOptions: {
15-
render: createDebouncedFn(() => renderDOMHead(head), fn => setTimeout(() => fn(), 0)),
16-
},
17-
...options,
18-
})
12+
const domRenderer = createDomRenderer({ document })
13+
let head: Unhead
14+
const debouncedRenderer = createDebouncedFn(() => domRenderer(head), fn => setTimeout(() => fn(), 0))
15+
head = _createClientHead({ document, render: debouncedRenderer, ...options })
1916
return head
2017
},
2118
}])

packages/react/src/client.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import type { ReactNode } from 'react'
22
import type { CreateClientHeadOptions, Unhead } from 'unhead/types'
33
import { createElement } from 'react'
4-
import { createHead as _createHead, createDebouncedFn, renderDOMHead } from 'unhead/client'
4+
import { createHead as _createHead, createDebouncedFn, createDomRenderer } from 'unhead/client'
55
import { UnheadContext } from './context'
66

77
export { renderDOMHead } from 'unhead/client'
88

99
export function createHead(options: CreateClientHeadOptions = {}): Unhead {
10-
const head = _createHead({
11-
domOptions: {
12-
render: createDebouncedFn(() => renderDOMHead(head), fn => setTimeout(fn, 0)),
13-
},
14-
...options,
15-
})
10+
const domRenderer = createDomRenderer()
11+
let head: Unhead
12+
const debouncedRenderer = createDebouncedFn(() => domRenderer(head), fn => setTimeout(fn, 0))
13+
head = _createHead({ render: debouncedRenderer, ...options })
1614
return head
1715
}
1816

packages/solid-js/src/client.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import type { CreateClientHeadOptions, Unhead } from 'unhead/types'
2-
import { createHead as _createHead, createDebouncedFn, renderDOMHead } from 'unhead/client'
2+
import { createHead as _createHead, createDebouncedFn, createDomRenderer } from 'unhead/client'
33

44
export { UnheadContext } from './context'
55

66
export { renderDOMHead } from 'unhead/client'
77

88
export function createHead(options: CreateClientHeadOptions = {}): Unhead {
9-
const head = _createHead({
10-
domOptions: {
11-
render: createDebouncedFn(() => renderDOMHead(head), fn => setTimeout(fn, 0)),
12-
},
13-
...options,
14-
})
9+
const domRenderer = createDomRenderer()
10+
let head: Unhead
11+
const debouncedRenderer = createDebouncedFn(() => domRenderer(head), fn => setTimeout(fn, 0))
12+
head = _createHead({ render: debouncedRenderer, ...options })
1513
return head
1614
}
1715

packages/svelte/src/client.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import type { CreateClientHeadOptions, Unhead } from 'unhead/types'
22
import { tick } from 'svelte'
3-
import { createHead as _createHead, createDebouncedFn, renderDOMHead } from 'unhead/client'
3+
import { createHead as _createHead, createDebouncedFn, createDomRenderer } from 'unhead/client'
44

55
export { UnheadContextKey } from './context'
66

77
export function createHead(options: CreateClientHeadOptions = {}): Unhead {
8-
const head = _createHead({
9-
domOptions: {
10-
render: createDebouncedFn(() => renderDOMHead(head), fn => tick().then(fn)),
11-
},
12-
...options,
13-
})
8+
const domRenderer = createDomRenderer()
9+
let head: Unhead
10+
const debouncedRenderer = createDebouncedFn(() => domRenderer(head), fn => tick().then(fn))
11+
head = _createHead({ render: debouncedRenderer, ...options })
1412
return head
1513
}
1614

packages/unhead/README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ useHead({
5454
### Server-Side Rendering
5555

5656
```ts
57-
import { createHead, renderSSRHead } from 'unhead/server'
57+
import { createHead } from 'unhead/server'
5858

5959
const head = createHead()
6060

@@ -65,25 +65,38 @@ useHead({
6565
}, { head })
6666

6767
// Render head tags
68-
const { headTags, bodyTags } = renderSSRHead(head)
68+
const { headTags, bodyTags } = head.render()
6969
```
7070

71-
### Client-Side Hydration
71+
### Client-Side Rendering
7272

7373
```ts
74-
import { createHead, renderDOMHead } from 'unhead/client'
74+
import { createHead } from 'unhead/client'
7575

76+
// Create head instance (auto-renders on changes)
7677
const head = createHead()
7778

78-
// Enable DOM rendering
79-
renderDOMHead(head)
80-
8179
// Add reactive head entries
8280
useHead({
8381
title: 'Client App'
8482
}, { head })
8583
```
8684

85+
### Custom Renderers
86+
87+
```ts
88+
import { createServerRenderer, createUnhead } from 'unhead/server'
89+
90+
// Create a custom renderer with options
91+
const renderer = createServerRenderer({ omitLineBreaks: true })
92+
93+
// Pass renderer to createUnhead
94+
const head = createUnhead(renderer, { /* options */ })
95+
96+
// Render using head.render()
97+
const html = head.render()
98+
```
99+
87100
## Framework Integrations
88101

89102
Unhead provides optimized integrations for popular frameworks:
Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,93 @@
1-
import type { CreateClientHeadOptions, HeadTag, ResolvableHead } from '../types'
2-
import { createUnhead } from '../unhead'
1+
import type { ActiveHeadEntry, CreateClientHeadOptions, HeadEntryOptions, HeadRenderer, HeadTag, ResolvableHead, Unhead } from '../types'
2+
import { createUnhead, registerPlugin } from '../unhead'
33
import { TagPriorityAliases } from '../utils/const'
4-
import { renderDOMHead } from './renderDOMHead'
4+
import { createDomRenderer } from './renderDOMHead'
55

66
function tagWeight(tag: HeadTag) {
77
return typeof tag.tagPriority === 'number'
88
? tag.tagPriority
99
: 100 + (TagPriorityAliases[tag.tagPriority as keyof typeof TagPriorityAliases] || 0)
1010
}
1111

12-
export function createHead<T = ResolvableHead>(options: CreateClientHeadOptions = {}) {
13-
const render = options.domOptions?.render || renderDOMHead
12+
export interface ClientUnhead<T = ResolvableHead> extends Unhead<T, boolean> {
13+
dirty: boolean
14+
invalidate: () => void
15+
}
16+
17+
export function createHead<T = ResolvableHead>(options: CreateClientHeadOptions = {}): ClientUnhead<T> {
1418
options.document = options.document || (typeof window !== 'undefined' ? document : undefined)
19+
const renderer = (options.render || createDomRenderer({ document: options.document })) as HeadRenderer<boolean>
1520
const initialPayload = options.document?.head.querySelector('script[id="unhead:payload"]')?.innerHTML || false
16-
// restore initial entry from payload (titleTemplate and templateParams)
17-
return createUnhead<T>({
21+
22+
const core = createUnhead<T, boolean>(renderer, {
1823
...options,
1924
_tagWeight: tagWeight,
20-
plugins: [
21-
...(options.plugins || []),
22-
{
23-
key: 'client',
24-
hooks: {
25-
'entries:updated': (head) => { render(head) },
25+
plugins: [], // register on wrapped head instead
26+
init: [], // push on wrapped head instead
27+
})
28+
29+
let dirty = false
30+
31+
const head: ClientUnhead<T> = {
32+
...core,
33+
get dirty() { return dirty },
34+
set dirty(v) { dirty = v },
35+
render() {
36+
return renderer(head)
37+
},
38+
invalidate() {
39+
for (const entry of core.entries.values()) {
40+
entry._dirty = true
41+
}
42+
dirty = true
43+
core.hooks.callHook('entries:updated', head)
44+
},
45+
push(input: T, _options?: HeadEntryOptions) {
46+
const active = core.push(input, _options)
47+
const entry = core.entries.get(active._i)!
48+
entry._dirty = true
49+
dirty = true
50+
core.hooks.callHook('entries:updated', head)
51+
52+
const corePatch = active.patch
53+
const coreDispose = active.dispose
54+
55+
const clientActive: ActiveHeadEntry<T> = {
56+
_i: active._i,
57+
patch(input) {
58+
corePatch(input)
59+
entry._dirty = true
60+
dirty = true
61+
core.hooks.callHook('entries:updated', head)
62+
},
63+
dispose() {
64+
if (core.entries.has(active._i)) {
65+
coreDispose()
66+
head.invalidate()
67+
}
2668
},
27-
},
28-
],
29-
init: [
30-
initialPayload ? JSON.parse(initialPayload) : false,
31-
...(options.init || []),
32-
],
69+
}
70+
return clientActive
71+
},
72+
}
73+
74+
// register plugins on wrapped head
75+
;(options.plugins || []).forEach(p => registerPlugin(head, p))
76+
77+
// auto-render on entries:updated
78+
registerPlugin(head, {
79+
key: 'client',
80+
hooks: {
81+
'entries:updated': () => { head.render() },
82+
},
3383
})
84+
85+
// push init entries
86+
const initEntries = [
87+
initialPayload ? JSON.parse(initialPayload) : false,
88+
...(options.init || []),
89+
]
90+
initEntries.forEach(e => e && head.push(e as T))
91+
92+
return head
3493
}
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
import type { RenderDomHeadOptions, Unhead } from '../types'
2+
import { renderDOMHead as _renderDOMHead } from './renderDOMHead'
3+
14
export type {
25
CreateClientHeadOptions,
36
Unhead,
47
} from '../types'
58
export { createHead } from './createHead'
6-
export { renderDOMHead } from './renderDOMHead'
9+
export { createDomRenderer } from './renderDOMHead'
710
export { createDebouncedFn } from './util'
11+
12+
/** @deprecated Use `head.render()` instead */
13+
export function renderDOMHead(head: Unhead<any>, options?: RenderDomHeadOptions): boolean {
14+
return _renderDOMHead(head, options)
15+
}

packages/unhead/src/client/renderDOMHead.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
DomBeforeRenderCtx,
33
DomRenderTagContext,
44
DomState,
5+
HeadRenderer,
56
HeadTag,
67
RenderDomHeadOptions,
78
Unhead,
@@ -11,10 +12,20 @@ import { dedupeKey, hashTag, isMetaArrayDupeKey } from '../utils/dedupe'
1112
import { normalizeProps } from '../utils/normalize'
1213
import { resolveTags } from '../utils/resolve'
1314

15+
/* @__NO_SIDE_EFFECTS__ */
16+
export function createDomRenderer(options: RenderDomHeadOptions = {}): HeadRenderer<boolean> {
17+
return (head: Unhead<any>) => _renderDOMHead(head, options)
18+
}
19+
1420
/**
1521
* Render the head tags to the DOM.
22+
* @deprecated Use `head.render()` instead.
1623
*/
1724
export function renderDOMHead<T extends Unhead<any>>(head: T, options: RenderDomHeadOptions = {}): boolean {
25+
return _renderDOMHead(head, options)
26+
}
27+
28+
function _renderDOMHead<T extends Unhead<any>>(head: T, options: RenderDomHeadOptions = {}): boolean {
1829
const dom: Document | undefined = options.document || head.resolvedOptions.document
1930
if (!dom || !head.dirty)
2031
return false

packages/unhead/src/server/createHead.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { CreateServerHeadOptions, ResolvableHead } from '../types'
1+
import type { CreateServerHeadOptions, ResolvableHead, SSRHeadPayload } from '../types'
22
import { createUnhead } from '../unhead'
3+
import { createServerRenderer } from './renderSSRHead'
34
import { capoTagWeight } from './sort'
45

56
/* @__NO_SIDE_EFFECTS__ */
67
export function createHead<T = ResolvableHead>(options: CreateServerHeadOptions = {}) {
7-
const unhead = createUnhead<T>({
8+
const unhead = createUnhead<T, SSRHeadPayload>(createServerRenderer({ tagWeight: capoTagWeight }), {
89
...options,
910
_tagWeight: capoTagWeight,
1011
// @ts-expect-error untyped
@@ -47,9 +48,7 @@ export function createHead<T = ResolvableHead>(options: CreateServerHeadOptions
4748
if (Object.keys(unhead._ssrPayload || {}).length > 0) {
4849
payload = { ...unhead._ssrPayload }
4950
}
50-
// filter non-values
5151
if (Object.values(payload).some(Boolean)) {
52-
// add tag for rendering
5352
ctx.tags.push({
5453
tag: 'script',
5554
innerHTML: JSON.stringify(payload),
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import type { RenderSSRHeadOptions, SSRHeadPayload, Unhead } from '../types'
2+
import { renderSSRHead as _renderSSRHead } from './renderSSRHead'
3+
14
export type { CreateServerHeadOptions, SSRHeadPayload, Unhead } from '../types'
25
export { createHead } from './createHead'
3-
export { renderSSRHead } from './renderSSRHead'
6+
export { createServerRenderer } from './renderSSRHead'
47
export { capoTagWeight } from './sort'
58
export { transformHtmlTemplate, transformHtmlTemplateRaw } from './transformHtmlTemplate'
69
export { escapeHtml, propsToString, ssrRenderTags, tagToString } from './util'
10+
11+
/** @deprecated Use `head.render()` instead */
12+
export function renderSSRHead(head: Unhead<any>, options?: RenderSSRHeadOptions): SSRHeadPayload {
13+
return _renderSSRHead(head, options)
14+
}

0 commit comments

Comments
 (0)