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 ( + + + + + ) +} From aa39d52b0ffe34bd571e92d4223e94630ee2e26f Mon Sep 17 00:00:00 2001 From: pieh Date: Tue, 17 Jan 2023 10:53:04 +0100 Subject: [PATCH 03/15] get rid of debug logs --- packages/gatsby/cache-dir/static-entry.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index 25fc0e7a11ed5..94fe2975cd9ea 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -177,13 +177,11 @@ 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) } From a8f5da9f6648f21f7ff2b4668e63a9c45908baa6 Mon Sep 17 00:00:00 2001 From: pieh Date: Tue, 17 Jan 2023 10:53:24 +0100 Subject: [PATCH 04/15] setBody/HtmlAttributes doesn't have second arg --- packages/gatsby/cache-dir/head/head-export-handler-for-ssr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fd3e00c5dedf8..c5dab13eb53eb 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 @@ -67,12 +67,12 @@ export function headHandlerForSSR({ } if (rawTagName === `html`) { - setHtmlAttributes(node.attributes, true) + setHtmlAttributes(node.attributes) continue } if (rawTagName === `body`) { - setBodyAttributes(node.attributes, true) + setBodyAttributes(node.attributes) continue } From f20ad4c43379d20f4898f1da69d7636def4428fd Mon Sep 17 00:00:00 2001 From: pieh Date: Tue, 17 Jan 2023 11:20:09 +0100 Subject: [PATCH 05/15] drop another console.log --- packages/gatsby/cache-dir/ssr-develop-static-entry.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gatsby/cache-dir/ssr-develop-static-entry.js b/packages/gatsby/cache-dir/ssr-develop-static-entry.js index 00d77baa99813..dc3b0401cce23 100644 --- a/packages/gatsby/cache-dir/ssr-develop-static-entry.js +++ b/packages/gatsby/cache-dir/ssr-develop-static-entry.js @@ -98,7 +98,6 @@ export default async function staticPage({ } const setHtmlAttributes = attributes => { - console.log(`This got called`) htmlAttributes = merge(htmlAttributes, attributes) } From b2172017e03b82480997fbfbcb6ffb42f6f0742c Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 09:12:29 +0100 Subject: [PATCH 06/15] add test to e2e/dev --- .../html-and-body-attributes.js | 50 +++++++++++++++++++ .../shared-data/head-function-export.js | 3 +- .../html-and-body-attributes.js | 28 +++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 e2e-tests/development-runtime/cypress/integration/head-function-export/html-and-body-attributes.js create mode 100644 e2e-tests/development-runtime/src/pages/head-function-export/html-and-body-attributes.js diff --git a/e2e-tests/development-runtime/cypress/integration/head-function-export/html-and-body-attributes.js b/e2e-tests/development-runtime/cypress/integration/head-function-export/html-and-body-attributes.js new file mode 100644 index 0000000000000..3919e085a10cd --- /dev/null +++ b/e2e-tests/development-runtime/cypress/integration/head-function-export/html-and-body-attributes.js @@ -0,0 +1,50 @@ +import headFunctionExportSharedData from "../../../shared-data/head-function-export.js" + +describe(`Html and body attributes`, () => { + it(`Page has body and html attributes on direct visit`, () => { + cy.visit( + headFunctionExportSharedData.page.htmlAndBodyAttributes + ).waitForRouteChange() + + cy.get(`body`).should(`have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`have.attr`, `class`, `foo`) + cy.get(`html`).should(`have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`have.attr`, `lang`, `fr`) + }) + + it(`Page has body and html attributes on client-side navigation`, () => { + cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange() + + cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`not.have.attr`, `class`, `foo`) + cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`not.have.attr`, `lang`, `fr`) + + cy.visit( + headFunctionExportSharedData.page.htmlAndBodyAttributes + ).waitForRouteChange() + + cy.get(`body`).should(`have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`have.attr`, `class`, `foo`) + cy.get(`html`).should(`have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`have.attr`, `lang`, `fr`) + }) + + it(`Body and html attributes are removed on client-side navigation when new page doesn't set them`, () => { + cy.visit( + headFunctionExportSharedData.page.htmlAndBodyAttributes + ).waitForRouteChange() + + cy.get(`body`).should(`have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`have.attr`, `class`, `foo`) + cy.get(`html`).should(`have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`have.attr`, `lang`, `fr`) + + cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange() + + cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`not.have.attr`, `class`, `foo`) + cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`not.have.attr`, `lang`, `fr`) + }) +}) diff --git a/e2e-tests/development-runtime/shared-data/head-function-export.js b/e2e-tests/development-runtime/shared-data/head-function-export.js index c40d4272a2c01..b0d3aede0fcac 100644 --- a/e2e-tests/development-runtime/shared-data/head-function-export.js +++ b/e2e-tests/development-runtime/shared-data/head-function-export.js @@ -12,6 +12,7 @@ const page = { invalidElements: `${path}/invalid-elements/`, fsRouteApi: `${path}/fs-route-api/`, deduplication: `${path}/deduplication/`, + htmlAndBodyAttributes: `${path}/html-and-body-attributes/`, } const data = { @@ -23,7 +24,7 @@ const data = { style: `rebeccapurple`, link: `/used-by-head-function-export-basic.css`, extraMeta: `Extra meta tag that should be removed during navigation`, - jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}` + jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}`, }, queried: { base: `http://localhost:8000`, diff --git a/e2e-tests/development-runtime/src/pages/head-function-export/html-and-body-attributes.js b/e2e-tests/development-runtime/src/pages/head-function-export/html-and-body-attributes.js new file mode 100644 index 0000000000000..837b8f7ee15d1 --- /dev/null +++ b/e2e-tests/development-runtime/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 ( + + + + + ) +} From 912b8642f11cdce4bc6073bdc4d35d4077d45db8 Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 09:31:50 +0100 Subject: [PATCH 07/15] add test to e2e/prod --- .../html-and-body-attributes.js | 50 +++++++++++++++++++ .../shared-data/head-function-export.js | 3 +- .../html-and-body-attributes.js | 28 +++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js create mode 100644 e2e-tests/production-runtime/src/pages/head-function-export/html-and-body-attributes.js diff --git a/e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js b/e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js new file mode 100644 index 0000000000000..3919e085a10cd --- /dev/null +++ b/e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js @@ -0,0 +1,50 @@ +import headFunctionExportSharedData from "../../../shared-data/head-function-export.js" + +describe(`Html and body attributes`, () => { + it(`Page has body and html attributes on direct visit`, () => { + cy.visit( + headFunctionExportSharedData.page.htmlAndBodyAttributes + ).waitForRouteChange() + + cy.get(`body`).should(`have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`have.attr`, `class`, `foo`) + cy.get(`html`).should(`have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`have.attr`, `lang`, `fr`) + }) + + it(`Page has body and html attributes on client-side navigation`, () => { + cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange() + + cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`not.have.attr`, `class`, `foo`) + cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`not.have.attr`, `lang`, `fr`) + + cy.visit( + headFunctionExportSharedData.page.htmlAndBodyAttributes + ).waitForRouteChange() + + cy.get(`body`).should(`have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`have.attr`, `class`, `foo`) + cy.get(`html`).should(`have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`have.attr`, `lang`, `fr`) + }) + + it(`Body and html attributes are removed on client-side navigation when new page doesn't set them`, () => { + cy.visit( + headFunctionExportSharedData.page.htmlAndBodyAttributes + ).waitForRouteChange() + + cy.get(`body`).should(`have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`have.attr`, `class`, `foo`) + cy.get(`html`).should(`have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`have.attr`, `lang`, `fr`) + + cy.visit(headFunctionExportSharedData.page.basic).waitForRouteChange() + + cy.get(`body`).should(`not.have.attr`, `data-foo`, `baz`) + cy.get(`body`).should(`not.have.attr`, `class`, `foo`) + cy.get(`html`).should(`not.have.attr`, `data-foo`, `bar`) + cy.get(`html`).should(`not.have.attr`, `lang`, `fr`) + }) +}) diff --git a/e2e-tests/production-runtime/shared-data/head-function-export.js b/e2e-tests/production-runtime/shared-data/head-function-export.js index 6dd60789408d8..5e3914fb63727 100644 --- a/e2e-tests/production-runtime/shared-data/head-function-export.js +++ b/e2e-tests/production-runtime/shared-data/head-function-export.js @@ -13,6 +13,7 @@ const page = { fsRouteApi: `${path}/fs-route-api/`, deduplication: `${path}/deduplication/`, pageWithUseLocation: `${path}/page-with-uselocation/`, + htmlAndBodyAttributes: `${path}/html-and-body-attributes/`, } const data = { @@ -24,7 +25,7 @@ const data = { style: `rebeccapurple`, link: `/used-by-head-function-export-basic.css`, extraMeta: `Extra meta tag that should be removed during navigation`, - jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}` + jsonLD: `{"@context":"https://schema.org","@type":"Organization","url":"https://www.spookytech.com","name":"Spookytechnologies","contactPoint":{"@type":"ContactPoint","telephone":"+5-601-785-8543","contactType":"CustomerSupport"}}`, }, queried: { base: `http://localhost:9000`, diff --git a/e2e-tests/production-runtime/src/pages/head-function-export/html-and-body-attributes.js b/e2e-tests/production-runtime/src/pages/head-function-export/html-and-body-attributes.js new file mode 100644 index 0000000000000..837b8f7ee15d1 --- /dev/null +++ b/e2e-tests/production-runtime/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 ( + + + + + ) +} From 8eaac11a902500d644126a759c6d23a9bc520ba1 Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 09:39:12 +0100 Subject: [PATCH 08/15] sigh ... silence invalid nesting of html and body elements --- .../head/head-export-handler-for-browser.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 bee9fe48e8c01..54a564f13c8f5 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 @@ -126,6 +126,25 @@ const onHeadRendered = () => { } if (process.env.BUILD_STAGE === `develop`) { + // sigh ... and elements are not valid descedents of
(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
, and just silence warnings for and 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] === `` || args[1] === ``) + ) { + return + } + return originalConsoleError(...args) + } + // We set up observer to be able to regenerate after react-refresh // updates our hidden element. const observer = new MutationObserver(onHeadRendered) From a7796cc1def0a6878e53428b655904079d19084f Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 09:43:51 +0100 Subject: [PATCH 09/15] add comment about order of onRenderBody vs Head --- packages/gatsby/cache-dir/ssr-develop-static-entry.js | 2 ++ packages/gatsby/cache-dir/static-entry.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/gatsby/cache-dir/ssr-develop-static-entry.js b/packages/gatsby/cache-dir/ssr-develop-static-entry.js index dc3b0401cce23..9826cc56f25f2 100644 --- a/packages/gatsby/cache-dir/ssr-develop-static-entry.js +++ b/packages/gatsby/cache-dir/ssr-develop-static-entry.js @@ -306,6 +306,8 @@ export default async function staticPage({ pathname: pagePath, }) + // we want to run Head after onRenderBody, so Html and Body attributes + // from Head wins over global ones from onRenderBody headHandlerForSSR({ pageComponent, setHeadComponents, diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index 94fe2975cd9ea..4ff5b5b99d15e 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -368,6 +368,8 @@ export default async function staticPage({ pathPrefix: __PATH_PREFIX__, }) + // we want to run Head after onRenderBody, so Html and Body attributes + // from Head wins over global ones from onRenderBody headHandlerForSSR({ pageComponent, setHeadComponents, From 02f5d67a3553dfaac72f42070ba4d16c885b8684 Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 09:50:17 +0100 Subject: [PATCH 10/15] consistent return --- .../gatsby/cache-dir/head/head-export-handler-for-browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 54a564f13c8f5..bbf99eb160f0e 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 @@ -140,7 +140,7 @@ if (process.env.BUILD_STAGE === `develop`) { args[0]?.includes(`validateDOMNesting(...): %s cannot appear as`) && (args[1] === `` || args[1] === ``) ) { - return + return undefined } return originalConsoleError(...args) } From 54e3fb769f68973c8dafd32adff477f2cae0a156 Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 10:32:00 +0100 Subject: [PATCH 11/15] dev ssr tests --- integration-tests/ssr/__tests__/ssr.js | 18 ++++++++++++++++++ .../ssr/src/pages/head-function-export.js | 8 +++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/integration-tests/ssr/__tests__/ssr.js b/integration-tests/ssr/__tests__/ssr.js index 98cfb68f3f19b..af57d3ec849cb 100644 --- a/integration-tests/ssr/__tests__/ssr.js +++ b/integration-tests/ssr/__tests__/ssr.js @@ -64,6 +64,24 @@ describe(`SSR`, () => { const ssrHead = ssrDom.querySelector(`[data-testid=title]`) expect(devSsrHead.textContent).toEqual(ssrHead.textContent) + expect(devSsrDom.querySelector(`html`).attributes).toEqual( + ssrDom.querySelector(`html`).attributes + ) + expect(devSsrDom.querySelector(`html`).attributes).toMatchInlineSnapshot(` + Object { + "data-foo": "bar", + "lang": "fr", + } + `) + + expect(devSsrDom.querySelector(`body`).attributes).toEqual( + ssrDom.querySelector(`body`).attributes + ) + expect(devSsrDom.querySelector(`body`).attributes).toMatchInlineSnapshot(` + Object { + "data-foo": "baz", + } + `) }) describe(`it generates an error page correctly`, () => { diff --git a/integration-tests/ssr/src/pages/head-function-export.js b/integration-tests/ssr/src/pages/head-function-export.js index 94813eb0bdffc..0894295f4fe24 100644 --- a/integration-tests/ssr/src/pages/head-function-export.js +++ b/integration-tests/ssr/src/pages/head-function-export.js @@ -5,5 +5,11 @@ export default function PageWithHeadFunctionExport() { } export function Head() { - return Hello world + return ( + <> + + + Hello world + + ) } From e61c5ec8b273e7c8b2a11dde9635a87fa0212376 Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 10:34:39 +0100 Subject: [PATCH 12/15] offline ... --- .../head-function-export/html-and-body-attributes.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js b/e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js index 3919e085a10cd..f8b7c44f1286c 100644 --- a/e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js +++ b/e2e-tests/production-runtime/cypress/integration/head-function-export/html-and-body-attributes.js @@ -1,5 +1,16 @@ import headFunctionExportSharedData from "../../../shared-data/head-function-export.js" +Cypress.on("uncaught:exception", err => { + if ( + (err.message.includes("Minified React error #418") || + err.message.includes("Minified React error #423") || + err.message.includes("Minified React error #425")) && + Cypress.env(`TEST_PLUGIN_OFFLINE`) + ) { + return false + } +}) + describe(`Html and body attributes`, () => { it(`Page has body and html attributes on direct visit`, () => { cy.visit( From 753165e10492164bec0c43134b2079cd46a66b85 Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 11:17:10 +0100 Subject: [PATCH 13/15] offline ... vol2 --- .../head-function-export/html-insertion.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/e2e-tests/production-runtime/cypress/integration/head-function-export/html-insertion.js b/e2e-tests/production-runtime/cypress/integration/head-function-export/html-insertion.js index 0c34c9d59502c..17f56a26e7858 100644 --- a/e2e-tests/production-runtime/cypress/integration/head-function-export/html-insertion.js +++ b/e2e-tests/production-runtime/cypress/integration/head-function-export/html-insertion.js @@ -1,5 +1,16 @@ import { page, data } from "../../../shared-data/head-function-export.js" +Cypress.on("uncaught:exception", err => { + if ( + (err.message.includes("Minified React error #418") || + err.message.includes("Minified React error #423") || + err.message.includes("Minified React error #425")) && + Cypress.env(`TEST_PLUGIN_OFFLINE`) + ) { + return false + } +}) + describe(`Head function export html insertion`, () => { it(`should work with static data`, () => { cy.visit(page.basic).waitForRouteChange() From aea0a82b96d8b8a31719cf4f0a68e68696995cec Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 19:43:30 +0100 Subject: [PATCH 14/15] fix tracking body attributes --- .../head/head-export-handler-for-browser.js | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 bbf99eb160f0e..6b8053d06997e 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 @@ -30,7 +30,12 @@ const removePrevBodyAttributes = () => { }) } -const updateAttribute = (tagName, attributeName, attributeValue) => { +const updateAttribute = ( + tagName, + attributeName, + attributeValue, + attributesList +) => { const elementTag = document.getElementsByTagName(tagName)[0] if (!elementTag) { @@ -38,7 +43,7 @@ const updateAttribute = (tagName, attributeName, attributeValue) => { } elementTag.setAttribute(attributeName, attributeValue) - htmlAttributesList.add(attributeName) + attributesList.add(attributeName) } const removePrevHeadElements = () => { @@ -64,14 +69,24 @@ const onHeadRendered = () => { if (nodeName === `html`) { for (const attribute of node.attributes) { - updateAttribute(`html`, attribute.name, attribute.value) + updateAttribute( + `html`, + attribute.name, + attribute.value, + htmlAttributesList + ) } continue } if (nodeName === `body`) { for (const attribute of node.attributes) { - updateAttribute(`body`, attribute.name, attribute.value) + updateAttribute( + `body`, + attribute.name, + attribute.value, + bodyAttributesList + ) } continue } From 92c4f282f1bd252c4a86bd044bd97f1c0c903c58 Mon Sep 17 00:00:00 2001 From: pieh Date: Wed, 18 Jan 2023 19:43:48 +0100 Subject: [PATCH 15/15] fix deduplication --- .../gatsby/cache-dir/head/head-export-handler-for-browser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6b8053d06997e..1770cd391387a 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 @@ -117,9 +117,9 @@ const onHeadRendered = () => { continue } + } else { + validHeadNodes.push(clonedNode) } - - validHeadNodes.push(clonedNode) } const existingHeadElements = document.querySelectorAll(`[data-gatsby-head]`)