UNPKG

gatsby

Version:
129 lines (110 loc) 3.85 kB
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() } }) }