Skip to content

Commit 6e1dff2

Browse files
committedMar 7, 2025·
fix(unhead): init option should trigger DOM renders
Fixes #513
1 parent e7562b4 commit 6e1dff2

File tree

2 files changed

+132
-105
lines changed

2 files changed

+132
-105
lines changed
 

‎packages/unhead/src/client/createHead.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@ import { createUnhead } from '../unhead'
33
import { renderDOMHead } from './renderDOMHead'
44

55
export function createHead<T = ResolvableHead>(options: CreateClientHeadOptions = {}) {
6+
const render = options.domOptions?.render || renderDOMHead
7+
options.document = options.document || (typeof window !== 'undefined' ? document : undefined)
8+
const initialPayload = options.document?.head.querySelector('script[id="unhead:payload"]')?.innerHTML || false
69
// restore initial entry from payload (titleTemplate and templateParams)
7-
const head = createUnhead<T>({
8-
document: (typeof window !== 'undefined' ? document : undefined),
10+
return createUnhead<T>({
911
...options,
12+
plugins: [
13+
...(options.plugins || []),
14+
{
15+
key: 'client',
16+
hooks: {
17+
'entries:updated': render,
18+
},
19+
},
20+
],
21+
init: [
22+
JSON.parse(initialPayload || '{}'),
23+
...(options.init || []),
24+
],
1025
})
11-
// restore initial entry from payload (titleTemplate and templateParams)
12-
const initialPayload = head.resolvedOptions.document?.head.querySelector('script[id="unhead:payload"]')?.innerHTML || false
13-
if (initialPayload) {
14-
head.push(JSON.parse(initialPayload))
15-
}
16-
const render = options.domOptions?.render || renderDOMHead
17-
head.use({
18-
key: 'client',
19-
hooks: {
20-
'entries:updated': render,
21-
},
22-
})
23-
return head
2426
}

‎packages/unhead/test/unit/client/titleTemplate.test.ts

+115-90
Original file line numberDiff line numberDiff line change
@@ -3,126 +3,151 @@ import { createClientHeadWithContext, useDom } from '../../util'
33

44
describe('titleTemplate', () => {
55
it('string replace', async () => {
6-
const head = createClientHeadWithContext()
6+
const dom = useDom()
7+
const head = createClientHeadWithContext({
8+
document: dom.window.document,
9+
})
710
head.push({
811
titleTemplate: '%s - my template',
912
title: 'test',
1013
})
1114

12-
const dom = useDom()
1315
await renderDOMHead(head, { document: dom.window.document })
1416

1517
expect(dom.window.document.title).toMatchInlineSnapshot(
1618
`"test - my template"`,
1719
)
1820
})
1921
it('fn replace', async () => {
20-
const head = createClientHeadWithContext()
22+
const dom = useDom()
23+
const head = createClientHeadWithContext({
24+
document: dom.window.document,
25+
})
2126
head.push({
2227
titleTemplate: (title?: string) => `${title} - my template`,
2328
title: 'test',
2429
})
25-
const dom = useDom()
2630
await renderDOMHead(head, { document: dom.window.document })
2731
expect(dom.window.document.title).toMatchInlineSnapshot(
2832
`"test - my template"`,
2933
)
3034
})
3135
it('titleTemplate as title', async () => {
32-
const head = createClientHeadWithContext()
36+
const dom = useDom()
37+
const head = createClientHeadWithContext({
38+
document: dom.window.document,
39+
})
3340
head.push({
3441
titleTemplate: (title?: string) => title ? `${title} - Template` : 'Default Title',
3542
title: null,
3643
})
37-
const dom = useDom()
3844
await renderDOMHead(head, { document: dom.window.document })
3945
expect(dom.window.document.title).toMatchInlineSnapshot(
4046
`"Default Title"`,
4147
)
4248
})
4349
// TODO convert to client
44-
// it('titleTemplate as title - update', async () => {
45-
// const head = createClientHeadWithContext()
46-
// head.push({
47-
// titleTemplate: (title?: string) => title ? `${title} - Template` : 'Default Title',
48-
// })
49-
// expect((await renderSSRHead(head)).headTags).toMatchInlineSnapshot(
50-
// `"<title>Default Title</title>"`,
51-
// )
52-
// const entry = head.push({
53-
// title: 'Hello world',
54-
// })
55-
// expect((await renderSSRHead(head)).headTags).toMatchInlineSnapshot(
56-
// `"<title>Hello world - Template</title>"`,
57-
// )
58-
// entry.dispose()
59-
// expect((await renderSSRHead(head)).headTags).toMatchInlineSnapshot(
60-
// `"<title>Default Title</title>"`,
61-
// )
62-
// })
63-
// it('reset title template', async () => {
64-
// const head = createClientHeadWithContext()
65-
// head.push({
66-
// titleTemplate: (title?: string) => title ? `${title} - Template` : 'Default Title',
67-
// })
68-
// head.push({
69-
// titleTemplate: null,
70-
// title: 'page title',
71-
// })
72-
// const { headTags } = await renderSSRHead(head)
73-
// expect(headTags).toMatchInlineSnapshot(
74-
// `"<title>page title</title>"`,
75-
// )
76-
// })
77-
//
78-
// it('nested title template', async () => {
79-
// const head = createClientHeadWithContext()
80-
// head.push({
81-
// titleTemplate: (title?: string) => title ? `${title} - Template` : 'Default Title',
82-
// })
83-
// head.push({
84-
// titleTemplate: null,
85-
// })
86-
// const { headTags } = await renderSSRHead(head)
87-
// expect(headTags).toMatchInlineSnapshot(
88-
// '""',
89-
// )
90-
// })
91-
//
92-
// it('null fn return', async () => {
93-
// const head = createClientHeadWithContext()
94-
// head.push({
95-
// titleTemplate: (title?: string) => title === 'test' ? null : `${title} - Template`,
96-
// title: 'test',
97-
// })
98-
// const { headTags } = await renderSSRHead(head)
99-
// expect(headTags).toMatchInlineSnapshot(
100-
// '""',
101-
// )
102-
// })
103-
//
104-
// it('empty title', async () => {
105-
// const head = createClientHeadWithContext()
106-
// head.push({
107-
// title: '',
108-
// })
109-
// const { headTags } = await renderSSRHead(head)
110-
// expect(headTags).toMatchInlineSnapshot(
111-
// `""`,
112-
// )
113-
// })
114-
//
115-
// it('replacing title with empty', async () => {
116-
// const head = createClientHeadWithContext()
117-
// head.push({
118-
// title: 'test',
119-
// })
120-
// head.push({
121-
// title: '',
122-
// })
123-
// const { headTags } = await renderSSRHead(head)
124-
// expect(headTags).toMatchInlineSnapshot(
125-
// `""`,
126-
// )
127-
// })
50+
it('titleTemplate as title - update', async () => {
51+
const dom = useDom()
52+
const head = createClientHeadWithContext({
53+
document: dom.window.document,
54+
})
55+
head.push({
56+
titleTemplate: (title?: string) => title ? `${title} - Template` : 'Default Title',
57+
})
58+
await renderDOMHead(head, { document: dom.window.document })
59+
expect(dom.window.document.title).toMatchInlineSnapshot(`"Default Title"`)
60+
const entry = head.push({
61+
title: 'Hello world',
62+
})
63+
await renderDOMHead(head, { document: dom.window.document })
64+
expect(dom.window.document.title).toMatchInlineSnapshot(`"Hello world - Template"`)
65+
entry.dispose()
66+
await renderDOMHead(head, { document: dom.window.document })
67+
expect(dom.window.document.title).toMatchInlineSnapshot(`"Default Title"`)
68+
})
69+
it('reset title template', async () => {
70+
const dom = useDom()
71+
const head = createClientHeadWithContext({
72+
document: dom.window.document,
73+
})
74+
head.push({
75+
titleTemplate: (title?: string) => title ? `${title} - Template` : 'Default Title',
76+
})
77+
head.push({
78+
titleTemplate: null,
79+
title: 'page title',
80+
})
81+
await renderDOMHead(head, { document: dom.window.document })
82+
expect(dom.window.document.title).toMatchInlineSnapshot(`"page title"`)
83+
})
84+
85+
it('nested title template', async () => {
86+
const dom = useDom()
87+
const head = createClientHeadWithContext({
88+
document: dom.window.document,
89+
})
90+
head.push({
91+
titleTemplate: (title?: string) => title ? `${title} - Template` : 'Default Title',
92+
})
93+
head.push({
94+
titleTemplate: null,
95+
})
96+
await renderDOMHead(head, { document: dom.window.document })
97+
expect(dom.window.document.title).toMatchInlineSnapshot(`""`)
98+
})
99+
100+
it('null fn return', async () => {
101+
const dom = useDom()
102+
const head = createClientHeadWithContext({
103+
document: dom.window.document,
104+
})
105+
head.push({
106+
titleTemplate: (title?: string) => title === 'test' ? null : `${title} - Template`,
107+
title: 'test',
108+
})
109+
await renderDOMHead(head, { document: dom.window.document })
110+
expect(dom.window.document.title).toMatchInlineSnapshot(`""`)
111+
})
112+
113+
it('empty title', async () => {
114+
const dom = useDom()
115+
const head = createClientHeadWithContext({
116+
document: dom.window.document,
117+
})
118+
head.push({
119+
title: '',
120+
})
121+
await renderDOMHead(head, { document: dom.window.document })
122+
expect(dom.window.document.title).toMatchInlineSnapshot(`""`)
123+
})
124+
125+
it('replacing title with empty', async () => {
126+
const dom = useDom()
127+
const head = createClientHeadWithContext({
128+
document: dom.window.document,
129+
})
130+
head.push({
131+
title: 'test',
132+
})
133+
head.push({
134+
title: '',
135+
})
136+
await renderDOMHead(head, { document: dom.window.document })
137+
expect(dom.window.document.title).toMatchInlineSnapshot(`""`)
138+
})
139+
it('#513', async () => {
140+
const dom = useDom()
141+
createClientHeadWithContext({
142+
document: dom.window.document,
143+
init: [
144+
{ titleTemplate: title => `${title ?? ''} — Jetfly Group`.replace(/^ /, '') },
145+
],
146+
})
147+
148+
// wait
149+
await new Promise(resolve => setTimeout(resolve, 100))
150+
151+
expect(dom.window.document.title).toMatchInlineSnapshot(`"Jetfly Group"`)
152+
})
128153
})

0 commit comments

Comments
 (0)
Please sign in to comment.