Skip to content

Commit

Permalink
feat(gatsby): Allow <html> and <body> attributes to be updated fr…
Browse files Browse the repository at this point in the history
…om `Head` (#37449)

* allow usage of html and body tags in head

* add integration test for html and body attrs

* get rid of debug logs

* setBody/HtmlAttributes doesn't have second arg

* drop another console.log

* add test to e2e/dev

* add test to e2e/prod

* sigh ... silence invalid nesting of html and body elements

* add comment about order of onRenderBody vs Head

* consistent return

* dev ssr tests

* offline ...

* offline ... vol2

* fix tracking body attributes

* fix deduplication

Co-authored-by: pieh <misiek.piechowiak@gmail.com>
  • Loading branch information
marvinjude and pieh committed Jan 19, 2023
1 parent e4f841f commit fe65c29
Show file tree
Hide file tree
Showing 17 changed files with 431 additions and 75 deletions.
@@ -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`)
})
})
Expand Up @@ -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 = {
Expand All @@ -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`,
Expand Down
@@ -0,0 +1,28 @@
import * as React from "react"

export default function HeadFunctionHtmlAndBodyAttributes() {
return (
<>
<h1>I have html and body attributes</h1>
</>
)
}

function Indirection({ children }) {
return (
<>
<html lang="fr" />
<body className="foo" />
{children}
</>
)
}

export function Head() {
return (
<Indirection>
<html data-foo="bar" />
<body data-foo="baz" />
</Indirection>
)
}
@@ -0,0 +1,61 @@
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(
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`)
})
})
@@ -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()
Expand Down
Expand Up @@ -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 = {
Expand All @@ -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`,
Expand Down
@@ -0,0 +1,28 @@
import * as React from "react"

export default function HeadFunctionHtmlAndBodyAttributes() {
return (
<>
<h1>I have html and body attributes</h1>
</>
)
}

function Indirection({ children }) {
return (
<>
<html lang="fr" />
<body className="foo" />
{children}
</>
)
}

export function Head() {
return (
<Indirection>
<html data-foo="bar" />
<body data-foo="baz" />
</Indirection>
)
}
Expand Up @@ -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",
}
`)
})
})
Expand Up @@ -8,6 +8,7 @@ const page = {
warnings: `${path}/warnings/`,
allProps: `${path}/all-props/`,
deduplication: `${path}/deduplication/`,
bodyAndHtmlAttributes: `${path}/html-and-body-attributes/`,
}

const data = {
Expand Down
@@ -0,0 +1,28 @@
import * as React from "react"

export default function HeadFunctionHtmlAndBodyAttributes() {
return (
<>
<h1>I have html and body attributes</h1>
</>
)
}

function Indirection({ children }) {
return (
<>
<html lang="fr" />
<body className="foo" />
{children}
</>
)
}

export function Head() {
return (
<Indirection>
<html data-foo="bar" />
<body data-foo="baz" />
</Indirection>
)
}
18 changes: 18 additions & 0 deletions integration-tests/ssr/__tests__/ssr.js
Expand Up @@ -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`, () => {
Expand Down
8 changes: 7 additions & 1 deletion integration-tests/ssr/src/pages/head-function-export.js
Expand Up @@ -5,5 +5,11 @@ export default function PageWithHeadFunctionExport() {
}

export function Head() {
return <title data-testid="title">Hello world</title>
return (
<>
<html lang="fr" data-foo="bar" />
<body data-foo="baz" />
<title data-testid="title">Hello world</title>
</>
)
}
2 changes: 2 additions & 0 deletions packages/gatsby/cache-dir/head/constants.js
Expand Up @@ -6,4 +6,6 @@ export const VALID_NODE_NAMES = [
`base`,
`noscript`,
`script`,
`html`,
`body`,
]

0 comments on commit fe65c29

Please sign in to comment.