-
Notifications
You must be signed in to change notification settings - Fork 10.3k
/
head-export-handler-for-browser.js
95 lines (82 loc) · 2.79 KB
/
head-export-handler-for-browser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import React from "react"
import { useEffect } from "react"
import { StaticQueryContext } from "gatsby"
import { reactDOMUtils } from "../react-dom-utils"
import { FireCallbackInEffect } from "./components/fire-callback-in-effect"
import { VALID_NODE_NAMES } from "./constants"
import {
headExportValidator,
filterHeadProps,
warnForInvalidTags,
} from "./utils"
const hiddenRoot = document.createElement(`div`)
const removePrevHeadElements = () => {
const prevHeadNodes = [...document.querySelectorAll(`[data-gatsby-head]`)]
prevHeadNodes.forEach(e => e.remove())
}
const onHeadRendered = () => {
const validHeadNodes = []
removePrevHeadElements()
const seenIds = new Map()
for (const node of hiddenRoot.childNodes) {
const nodeName = node.nodeName.toLowerCase()
const id = node.attributes.id?.value
if (!VALID_NODE_NAMES.includes(nodeName)) {
warnForInvalidTags(nodeName)
} else {
const clonedNode = node.cloneNode(true)
clonedNode.setAttribute(`data-gatsby-head`, true)
if (id) {
if (!seenIds.has(id)) {
validHeadNodes.push(clonedNode)
seenIds.set(id, validHeadNodes.length - 1)
} else {
const indexOfPreviouslyInsertedNode = seenIds.get(id)
validHeadNodes[indexOfPreviouslyInsertedNode].remove()
validHeadNodes[indexOfPreviouslyInsertedNode] = clonedNode
}
} else {
validHeadNodes.push(clonedNode)
}
}
}
document.head.append(...validHeadNodes)
}
if (process.env.BUILD_STAGE === `develop`) {
// We set up observer to be able to regenerate <head> after react-refresh
// updates our hidden element.
const observer = new MutationObserver(onHeadRendered)
observer.observe(hiddenRoot, {
attributes: true,
childList: true,
characterData: true,
subtree: true,
})
}
export function headHandlerForBrowser({
pageComponent,
staticQueryResults,
pageComponentProps,
}) {
useEffect(() => {
if (pageComponent?.Head) {
headExportValidator(pageComponent.Head)
const { render } = reactDOMUtils()
const Head = pageComponent.Head
render(
// just a hack to call the callback after react has done first render
// Note: In dev, we call onHeadRendered twice( in FireCallbackInEffect and after mutualution observer dectects initail render into hiddenRoot) this is for hot reloading
// In Prod we only call onHeadRendered in FireCallbackInEffect to render to head
<FireCallbackInEffect callback={onHeadRendered}>
<StaticQueryContext.Provider value={staticQueryResults}>
<Head {...filterHeadProps(pageComponentProps)} />
</StaticQueryContext.Provider>
</FireCallbackInEffect>,
hiddenRoot
)
}
return () => {
removePrevHeadElements()
}
})
}