gatsby
Version:
Blazing fast modern site generator for React
171 lines (150 loc) • 6.5 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 {
IsHeadRenderContext,
getValidHeadComponentReplacements,
} from "./components/head-components"
import {
headExportValidator,
filterHeadProps,
diffNodes,
getValidHeadNodesAndAttributes,
removePrevHeadElements,
applyHtmlAndBodyAttributes,
removeHtmlAndBodyAttributes,
} from "./utils"
import { apiRunner } from "../api-runner-browser"
const hiddenRoot = document.createElement(`div`)
const keysOfHtmlAndBodyAttributes = {
html: [],
body: [],
}
const onHeadRendered = () => {
const { validHeadNodes, htmlAndBodyAttributes } =
getValidHeadNodesAndAttributes(hiddenRoot)
keysOfHtmlAndBodyAttributes.html = Object.keys(htmlAndBodyAttributes.html)
keysOfHtmlAndBodyAttributes.body = Object.keys(htmlAndBodyAttributes.body)
applyHtmlAndBodyAttributes(htmlAndBodyAttributes)
/**
* The rest of the code block below is a diffing mechanism to ensure that
* the head elements aren't duplicated on every re-render.
*/
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`) {
// sigh ... <html> and <body> elements are not valid descedents of <div> (our hidden element)
// react-dom in dev mode will warn about this. There doesn't seem to be a way to render arbitrary
// user Head without hitting this issue (our hidden element could be just "new Document()", but
// this can only have 1 child, and we don't control what is being rendered so that's not an option)
// instead we continue to render to <div>, and just silence warnings for <html> and <body> elements
// https://github.com/facebook/react/blob/e2424f33b3ad727321fc12e75c5e94838e84c2b5/packages/react-dom-bindings/src/client/validateDOMNesting.js#L498-L520
const originalConsoleError = console.error.bind(console)
console.error = (...args) => {
if (
Array.isArray(args) &&
args.length >= 2 &&
args[0]?.includes?.(`validateDOMNesting(...): %s cannot appear as`) &&
(args[1] === `<html>` || args[1] === `<body>`)
) {
return undefined
}
return originalConsoleError(...args)
}
/* 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,
})
}
// React 19 does not allow rendering a second `<html>` or `<body>` anywhere in the tree:
// https://github.com/facebook/react/blob/50e7ec8a694072fd6fcd52182df8a75211bf084d/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js#L3739
// Unfortunately, our Head API relies on users being able to render these elements, which we then
// render in a hidden `<div>` to extract attributes to apply to the real `<html>` and `<body>`.
// We explored several alternatives, but none were viable given React's constraints.
// To work around this, we intercept attempts to render these elements inside of
// Head and replace them with custom components that render as `<div>`s with a
// `data-original-tag` attribute. We can then use this attribute later to apply
// attributes to the real `<html>` and `<body>` elements.
// Additionally other head elements are subject to React's new hoisting behavior,
// which has some differences from how Gatsby Head API works, so we do append `itemProp`
// which prevents React from hoisting those elements:
// https://github.com/facebook/react/blob/50e7ec8a694072fd6fcd52182df8a75211bf084d/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L5824
// we later will remove this attribute if it has our custom value before appending to real head.
// De-risk monkey patch by only applying it when needed (React 19+, not React 18)
const reactMajor = parseInt(React.version.split(`.`)[0], 10)
if (reactMajor !== 18 && !React.createElement.headPatched) {
const originalCreateElement = React.createElement
const validHeadComponentReplacements = getValidHeadComponentReplacements(
originalCreateElement,
false
)
React.createElement = function patchedCreateElement(type, props, ...rest) {
const headReplacement = validHeadComponentReplacements.get(type)
if (headReplacement) {
type = headReplacement
}
return originalCreateElement.call(React, type, props, ...rest)
}
React.createElement.headPatched = true
}
export function headHandlerForBrowser({
pageComponent,
staticQueryResults,
pageComponentProps,
}) {
useEffect(() => {
if (pageComponent?.Head) {
headExportValidator(pageComponent.Head)
const { render } = reactDOMUtils()
const HeadElement = (
<pageComponent.Head {...filterHeadProps(pageComponentProps)} />
)
const WrapHeadElement = apiRunner(
`wrapRootElement`,
{ element: HeadElement },
HeadElement,
({ result }) => {
return { element: result }
}
).pop()
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}>
<IsHeadRenderContext.Provider value={true}>
<StaticQueryContext.Provider value={staticQueryResults}>
<LocationProvider>{WrapHeadElement}</LocationProvider>
</StaticQueryContext.Provider>
</IsHeadRenderContext.Provider>
</FireCallbackInEffect>,
hiddenRoot
)
}
return () => {
removePrevHeadElements()
removeHtmlAndBodyAttributes(keysOfHtmlAndBodyAttributes)
}
})
}