From a9eca35799a647c6b72d6394b675229e9c098b56 Mon Sep 17 00:00:00 2001
From: Jude Agboola
Date: Thu, 12 Jan 2023 11:46:19 +0100
Subject: [PATCH 01/15] allow usage of html and body tags in head
---
packages/gatsby/cache-dir/head/constants.js | 2 +
.../head/head-export-handler-for-browser.js | 96 ++++++++++++++-----
.../head/head-export-handler-for-ssr.js | 74 ++++++++------
.../cache-dir/ssr-develop-static-entry.js | 19 ++--
packages/gatsby/cache-dir/static-entry.js | 21 ++--
5 files changed, 140 insertions(+), 72 deletions(-)
diff --git a/packages/gatsby/cache-dir/head/constants.js b/packages/gatsby/cache-dir/head/constants.js
index a0d86cfc26d73..2f5f22c852898 100644
--- a/packages/gatsby/cache-dir/head/constants.js
+++ b/packages/gatsby/cache-dir/head/constants.js
@@ -6,4 +6,6 @@ export const VALID_NODE_NAMES = [
`base`,
`noscript`,
`script`,
+ `html`,
+ `body`,
]
diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js
index 48a58ca27d3f2..bee9fe48e8c01 100644
--- a/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js
+++ b/packages/gatsby/cache-dir/head/head-export-handler-for-browser.js
@@ -13,6 +13,33 @@ import {
} from "./utils"
const hiddenRoot = document.createElement(`div`)
+const htmlAttributesList = new Set()
+const bodyAttributesList = new Set()
+
+const removePrevHtmlAttributes = () => {
+ htmlAttributesList.forEach(attributeName => {
+ const elementTag = document.getElementsByTagName(`html`)[0]
+ elementTag.removeAttribute(attributeName)
+ })
+}
+
+const removePrevBodyAttributes = () => {
+ bodyAttributesList.forEach(attributeName => {
+ const elementTag = document.getElementsByTagName(`body`)[0]
+ elementTag.removeAttribute(attributeName)
+ })
+}
+
+const updateAttribute = (tagName, attributeName, attributeValue) => {
+ const elementTag = document.getElementsByTagName(tagName)[0]
+
+ if (!elementTag) {
+ return
+ }
+
+ elementTag.setAttribute(attributeName, attributeValue)
+ htmlAttributesList.add(attributeName)
+}
const removePrevHeadElements = () => {
const prevHeadNodes = document.querySelectorAll(`[data-gatsby-head]`)
@@ -24,43 +51,60 @@ const removePrevHeadElements = () => {
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
+ continue
+ }
+
+ if (nodeName === `html`) {
+ for (const attribute of node.attributes) {
+ updateAttribute(`html`, attribute.name, attribute.value)
}
+ continue
+ }
- 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 {
+ if (nodeName === `body`) {
+ for (const attribute of node.attributes) {
+ updateAttribute(`body`, attribute.name, attribute.value)
+ }
+ continue
+ }
+
+ 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
+
+ continue
}
}
+
+ validHeadNodes.push(clonedNode)
}
const existingHeadElements = document.querySelectorAll(`[data-gatsby-head]`)
@@ -123,6 +167,8 @@ export function headHandlerForBrowser({
return () => {
removePrevHeadElements()
+ removePrevHtmlAttributes()
+ removePrevBodyAttributes()
}
})
}
diff --git a/packages/gatsby/cache-dir/head/head-export-handler-for-ssr.js b/packages/gatsby/cache-dir/head/head-export-handler-for-ssr.js
index 0b77760b24ffe..fd3e00c5dedf8 100644
--- a/packages/gatsby/cache-dir/head/head-export-handler-for-ssr.js
+++ b/packages/gatsby/cache-dir/head/head-export-handler-for-ssr.js
@@ -15,6 +15,8 @@ const { VALID_NODE_NAMES } = require(`./constants`)
export function headHandlerForSSR({
pageComponent,
setHeadComponents,
+ setHtmlAttributes,
+ setBodyAttributes,
staticQueryContext,
pageData,
pagePath,
@@ -53,48 +55,60 @@ export function headHandlerForSSR({
const headNodes = parse(rawString).childNodes
const validHeadNodes = []
-
const seenIds = new Map()
+
for (const node of headNodes) {
const { rawTagName } = node
const id = node.attributes?.id
if (!VALID_NODE_NAMES.includes(rawTagName)) {
warnForInvalidTags(rawTagName)
+ continue
+ }
+
+ if (rawTagName === `html`) {
+ setHtmlAttributes(node.attributes, true)
+ continue
+ }
+
+ if (rawTagName === `body`) {
+ setBodyAttributes(node.attributes, true)
+ continue
+ }
+
+ let element
+ const attributes = { ...node.attributes, "data-gatsby-head": true }
+
+ if (rawTagName === `script` || rawTagName === `style`) {
+ element = (
+
+ )
} else {
- let element
- const attributes = { ...node.attributes, "data-gatsby-head": true }
- if (rawTagName === `script` || rawTagName === `style`) {
- element = (
-
+ element =
+ node.textContent.length > 0 ? (
+
+ {node.textContent}
+
+ ) : (
+
)
- } else {
- element =
- node.textContent.length > 0 ? (
-
- {node.textContent}
-
- ) : (
-
- )
- }
+ }
- if (id) {
- if (!seenIds.has(id)) {
- validHeadNodes.push(element)
- seenIds.set(id, validHeadNodes.length - 1)
- } else {
- const indexOfPreviouslyInsertedNode = seenIds.get(id)
- validHeadNodes[indexOfPreviouslyInsertedNode] = element
- }
- } else {
+ if (id) {
+ if (!seenIds.has(id)) {
validHeadNodes.push(element)
+ seenIds.set(id, validHeadNodes.length - 1)
+ } else {
+ const indexOfPreviouslyInsertedNode = seenIds.get(id)
+ validHeadNodes[indexOfPreviouslyInsertedNode] = element
}
+ } else {
+ validHeadNodes.push(element)
}
}
diff --git a/packages/gatsby/cache-dir/ssr-develop-static-entry.js b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
index 3b472acd3c700..00d77baa99813 100644
--- a/packages/gatsby/cache-dir/ssr-develop-static-entry.js
+++ b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
@@ -98,6 +98,7 @@ export default async function staticPage({
}
const setHtmlAttributes = attributes => {
+ console.log(`This got called`)
htmlAttributes = merge(htmlAttributes, attributes)
}
@@ -162,14 +163,6 @@ export default async function staticPage({
const pageComponent = await syncRequires.ssrComponents[componentChunkName]
- headHandlerForSSR({
- pageComponent,
- setHeadComponents,
- staticQueryContext: getStaticQueryResults(),
- pageData,
- pagePath,
- })
-
let scriptsAndStyles = flatten(
[`commons`].map(chunkKey => {
const fetchKey = `assetsByChunkName[${chunkKey}]`
@@ -314,6 +307,16 @@ export default async function staticPage({
pathname: pagePath,
})
+ headHandlerForSSR({
+ pageComponent,
+ setHeadComponents,
+ setHtmlAttributes,
+ setBodyAttributes,
+ staticQueryContext: getStaticQueryResults(),
+ pageData,
+ pagePath,
+ })
+
apiRunner(`onPreRenderHTML`, {
getHeadComponents,
replaceHeadComponents,
diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js
index f6674f978bdcb..25fc0e7a11ed5 100644
--- a/packages/gatsby/cache-dir/static-entry.js
+++ b/packages/gatsby/cache-dir/static-entry.js
@@ -1,7 +1,6 @@
const React = require(`react`)
const path = require(`path`)
const {
- renderToString,
renderToStaticMarkup,
renderToPipeableStream,
} = require(`react-dom/server`)
@@ -178,11 +177,13 @@ export default async function staticPage({
const setHtmlAttributes = attributes => {
// TODO - we should remove deep merges
+ console.log({ htmlAttributes, attributes })
htmlAttributes = deepMerge(htmlAttributes, attributes)
}
const setBodyAttributes = attributes => {
// TODO - we should remove deep merges
+ console.log({ bodyAttributes, attributes })
bodyAttributes = deepMerge(bodyAttributes, attributes)
}
@@ -224,14 +225,6 @@ export default async function staticPage({
const { componentChunkName, slicesMap } = pageData
const pageComponent = await asyncRequires.components[componentChunkName]()
- headHandlerForSSR({
- pageComponent,
- setHeadComponents,
- staticQueryContext,
- pageData,
- pagePath,
- })
-
class RouteHandler extends React.Component {
render() {
const props = {
@@ -377,6 +370,16 @@ export default async function staticPage({
pathPrefix: __PATH_PREFIX__,
})
+ headHandlerForSSR({
+ pageComponent,
+ setHeadComponents,
+ setHtmlAttributes,
+ setBodyAttributes,
+ staticQueryContext,
+ pageData,
+ pagePath,
+ })
+
reversedScripts.forEach(script => {
// Add preload/prefetch s magic comments
if (script.shouldGenerateLink) {
From 6d4ea06acfec3fb542a458112b11aeb4308ec9c0 Mon Sep 17 00:00:00 2001
From: pieh
Date: Tue, 17 Jan 2023 10:44:18 +0100
Subject: [PATCH 02/15] add integration test for html and body attrs
---
.../__tests__/ssr-html-output.js | 20 +++++++++++++
.../shared-data/head-function-export.js | 1 +
.../html-and-body-attributes.js | 28 +++++++++++++++++++
3 files changed, 49 insertions(+)
create mode 100644 integration-tests/head-function-export/src/pages/head-function-export/html-and-body-attributes.js
diff --git a/integration-tests/head-function-export/__tests__/ssr-html-output.js b/integration-tests/head-function-export/__tests__/ssr-html-output.js
index 2890ee427ef40..9f4f3288566af 100644
--- a/integration-tests/head-function-export/__tests__/ssr-html-output.js
+++ b/integration-tests/head-function-export/__tests__/ssr-html-output.js
@@ -105,4 +105,24 @@ describe(`Head function export SSR'ed HTML output`, () => {
// alternate links are not using id, so should have multiple instances
expect(dom.querySelectorAll(`link[rel=alternate]`)?.length).toEqual(2)
})
+
+ it(`should allow setting html and body attributes`, () => {
+ const html = readFileSync(
+ `${publicDir}${page.bodyAndHtmlAttributes}/index.html`
+ )
+ const dom = parse(html)
+ expect(dom.querySelector(`html`).attributes).toMatchInlineSnapshot(`
+ {
+ "data-foo": "bar",
+ "lang": "fr",
+ }
+ `)
+
+ expect(dom.querySelector(`body`).attributes).toMatchInlineSnapshot(`
+ {
+ "class": "foo",
+ "data-foo": "baz",
+ }
+ `)
+ })
})
diff --git a/integration-tests/head-function-export/shared-data/head-function-export.js b/integration-tests/head-function-export/shared-data/head-function-export.js
index 6485af3655ba6..ff920d499ce47 100644
--- a/integration-tests/head-function-export/shared-data/head-function-export.js
+++ b/integration-tests/head-function-export/shared-data/head-function-export.js
@@ -8,6 +8,7 @@ const page = {
warnings: `${path}/warnings/`,
allProps: `${path}/all-props/`,
deduplication: `${path}/deduplication/`,
+ bodyAndHtmlAttributes: `${path}/html-and-body-attributes/`,
}
const data = {
diff --git a/integration-tests/head-function-export/src/pages/head-function-export/html-and-body-attributes.js b/integration-tests/head-function-export/src/pages/head-function-export/html-and-body-attributes.js
new file mode 100644
index 0000000000000..837b8f7ee15d1
--- /dev/null
+++ b/integration-tests/head-function-export/src/pages/head-function-export/html-and-body-attributes.js
@@ -0,0 +1,28 @@
+import * as React from "react"
+
+export default function HeadFunctionHtmlAndBodyAttributes() {
+ return (
+ <>
+ I have html and body attributes
+ >
+ )
+}
+
+function Indirection({ children }) {
+ return (
+ <>
+
+
+ {children}
+ >
+ )
+}
+
+export function Head() {
+ return (
+
+
+