11import React from "react"
22import { useEffect } from "react"
3+ import semver from "semver"
34import { StaticQueryContext } from "gatsby"
45import { LocationProvider } from "@gatsbyjs/reach-router"
6+
57import { reactDOMUtils } from "../react-dom-utils"
68import { FireCallbackInEffect } from "./components/fire-callback-in-effect"
79import {
@@ -32,7 +34,7 @@ const onHeadRendered = () => {
3234
3335 /**
3436 * The rest of the code block below is a diffing mechanism to ensure that
35- * the head elements aren't duplicted on every re-render.
37+ * the head elements aren't duplicated on every re-render.
3638 */
3739 const existingHeadElements = document . querySelectorAll ( `[data-gatsby-head]` )
3840
@@ -84,6 +86,40 @@ if (process.env.BUILD_STAGE === `develop`) {
8486 } )
8587}
8688
89+ // TODO(serhalp): Expose these as a new public API to allows us to gradually phase out
90+ // the existing `<html>` and `<body>` features in the Head API, allowing us to remove
91+ // the various workounds we have in place for React 19.
92+ function Html ( props ) {
93+ return < div data-original-tag = "html" { ...props } />
94+ }
95+ function Body ( props ) {
96+ return < div data-original-tag = "body" { ...props } />
97+ }
98+
99+ // React 19 does not allow rendering a second `<html>` or `<body>` anywhere in the tree:
100+ // https://github.com/facebook/react/blob/50e7ec8a694072fd6fcd52182df8a75211bf084d/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js#L3739
101+ // Unfortunately, our Head API relies on users being able to render these elements, which we then
102+ // render in a hidden `<div>` to extract attributes to apply to the real `<html>` and `<body>`.
103+ // We explored several alternatives, but none were viable given React's constraints.
104+ // To work around this, we intercept attempts to render these elements inside of
105+ // Head and replace them with custom components that render as `<div>`s with a
106+ // `data-original-tag` attribute. We can then use this attribute later to apply
107+ // attributes to the real `<html>` and `<body>` elements.
108+ const IsHeadRenderContext = React . createContext ( false )
109+ // De-risk monkey patch by only applying it when needed
110+ if ( semver . gte ( React . version , `19.0.0` ) ) {
111+ const originalCreateElement = React . createElement
112+ React . createElement = ( type , ...rest ) => {
113+ if ( type === `html` || type === `body` ) {
114+ const isHeadRender = React . useContext ( IsHeadRenderContext )
115+ // De-risk monkey patch by only applying it within a `Head()` render.
116+ if ( isHeadRender ) {
117+ type = type === `html` ? Html : Body
118+ }
119+ }
120+ return originalCreateElement ( type , ...rest )
121+ }
122+ }
87123export function headHandlerForBrowser ( {
88124 pageComponent,
89125 staticQueryResults,
@@ -96,7 +132,13 @@ export function headHandlerForBrowser({
96132 const { render } = reactDOMUtils ( )
97133
98134 const HeadElement = (
99- < pageComponent . Head { ...filterHeadProps ( pageComponentProps ) } />
135+ // Wrap in an SVG to work around conflict between React 19's new document metadata tag
136+ // hoisting and Gatsby's own preexisting Head API. React will leave metadata tags (like
137+ // `<title>`) with an `<svg>` ancestor intact, letting Gatsby process them as usual. See
138+ // https://github.com/facebook/react/blob/fd524fe02a86c3e92a207d90da970941320f337f/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js#L765-L779.
139+ < svg data-gatsby-head-react-19-workaround = "true" >
140+ < pageComponent . Head { ...filterHeadProps ( pageComponentProps ) } />
141+ </ svg >
100142 )
101143
102144 const WrapHeadElement = apiRunner (
@@ -113,9 +155,11 @@ export function headHandlerForBrowser({
113155 // Note: In dev, we call onHeadRendered twice( in FireCallbackInEffect and after mutualution observer dectects initail render into hiddenRoot) this is for hot reloading
114156 // In Prod we only call onHeadRendered in FireCallbackInEffect to render to head
115157 < FireCallbackInEffect callback = { onHeadRendered } >
116- < StaticQueryContext . Provider value = { staticQueryResults } >
117- < LocationProvider > { WrapHeadElement } </ LocationProvider >
118- </ StaticQueryContext . Provider >
158+ < IsHeadRenderContext . Provider value = { true } >
159+ < StaticQueryContext . Provider value = { staticQueryResults } >
160+ < LocationProvider > { WrapHeadElement } </ LocationProvider >
161+ </ StaticQueryContext . Provider >
162+ </ IsHeadRenderContext . Provider >
119163 </ FireCallbackInEffect > ,
120164 hiddenRoot
121165 )
0 commit comments