gatsby
Version:
Blazing fast modern site generator for React
129 lines (110 loc) • 3.85 kB
JavaScript
import React from "react"
import { useEffect } from "react"
import { StaticQueryContext } from "gatsby"
import { LocationProvider } from "@gatsbyjs/reach-router"
import { reactDOMUtils } from "../react-dom-utils"
import { FireCallbackInEffect } from "./components/fire-callback-in-effect"
import { VALID_NODE_NAMES } from "./constants"
import {
headExportValidator,
filterHeadProps,
warnForInvalidTags,
diffNodes,
} from "./utils"
const hiddenRoot = document.createElement(`div`)
const removePrevHeadElements = () => {
const prevHeadNodes = document.querySelectorAll(`[data-gatsby-head]`)
for (const node of prevHeadNodes) {
node.parentNode.removeChild(node)
}
}
const onHeadRendered = () => {
const validHeadNodes = []
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 {
let clonedNode = node.cloneNode(true)
clonedNode.setAttribute(`data-gatsby-head`, true)
// Create an element for scripts to make script work
if (clonedNode.nodeName.toLowerCase() === `script`) {
const script = document.createElement(`script`)
for (const attr of clonedNode.attributes) {
script.setAttribute(attr.name, attr.value)
}
script.innerHTML = clonedNode.innerHTML
clonedNode = script
}
if (id) {
if (!seenIds.has(id)) {
validHeadNodes.push(clonedNode)
seenIds.set(id, validHeadNodes.length - 1)
} else {
const indexOfPreviouslyInsertedNode = seenIds.get(id)
validHeadNodes[indexOfPreviouslyInsertedNode].parentNode?.removeChild(
validHeadNodes[indexOfPreviouslyInsertedNode]
)
validHeadNodes[indexOfPreviouslyInsertedNode] = clonedNode
}
} else {
validHeadNodes.push(clonedNode)
}
}
}
const existingHeadElements = document.querySelectorAll(`[data-gatsby-head]`)
if (existingHeadElements.length === 0) {
document.head.append(...validHeadNodes)
return
}
const newHeadNodes = []
diffNodes({
oldNodes: existingHeadElements,
newNodes: validHeadNodes,
onStale: node => node.parentNode.removeChild(node),
onNew: node => newHeadNodes.push(node),
})
document.head.append(...newHeadNodes)
}
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}>
<LocationProvider>
<Head {...filterHeadProps(pageComponentProps)} />
</LocationProvider>
</StaticQueryContext.Provider>
</FireCallbackInEffect>,
hiddenRoot
)
}
return () => {
removePrevHeadElements()
}
})
}