Skip to content

Commit

Permalink
feat(gatsby): allow deduplicating head elements on id (#36138) (#36159
Browse files Browse the repository at this point in the history
)

* feat: deduplicate head elements on id attrbibute in browser

* feat: deduplicate head elements on id attrbibute in html gen

* page with head deduplication

* add test

* update comments to match current code

* Update e2e-tests/development-runtime/cypress/integration/head-function-export/deduplication.js

Co-authored-by: Jude Agboola <marvinjudehk@gmail.com>

* Update e2e-tests/development-runtime/cypress/integration/head-function-export/deduplication.js

* add test case to e2e-production

* add test case to head integration tests

Co-authored-by: Jude Agboola <marvinjudehk@gmail.com>
(cherry picked from commit b08ef18)

Co-authored-by: Michal Piechowiak <misiek.piechowiak@gmail.com>
  • Loading branch information
ViCo0TeCH and pieh committed Jul 18, 2022
1 parent 62d2a49 commit 6f0d46b
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 3 deletions.
@@ -0,0 +1,19 @@
import headFunctionExportSharedData from "../../../shared-data/head-function-export.js"

it(`Deduplicates multiple tags with same id`, () => {
cy.visit(headFunctionExportSharedData.page.deduplication).waitForRouteChange()

// deduplication link has id and should be deduplicated
cy.get(`link[rel=deduplication]`).should("have.length", 1)
// last deduplication link should win
cy.get(`link[rel=deduplication]`).should("have.attr", "href", "/bar")
// we should preserve id
cy.get(`link[rel=deduplication]`).should(
"have.attr",
"id",
"deduplication-test"
)

// alternate links are not using id, so should have multiple instances
cy.get(`link[rel=alternate]`).should("have.length", 2)
})
Expand Up @@ -11,6 +11,7 @@ const page = {
ssr: `${path}/ssr/`,
invalidElements: `${path}/invalid-elements/`,
fsRouteApi: `${path}/fs-route-api/`,
deduplication: `${path}/deduplication/`,
}

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

export default function HeadFunctionDeduplication() {
return (
<>
<h1>
I deduplicated Head elements by their <code>id</code>
</h1>
</>
)
}

function SEO({ children }) {
return (
<>
<link rel="deduplication" id="deduplication-test" href="/foo" />
<link
rel="alternate"
type="application/atom+xml"
title="RSS Feed"
href="/blog/news/atom"
/>
{children}
</>
)
}

export function Head() {
return (
<SEO>
<link rel="deduplication" id="deduplication-test" href="/bar" />
<link rel="alternate" hrefLang="de-DE" href="/de/" />
</SEO>
)
}
@@ -0,0 +1,19 @@
import headFunctionExportSharedData from "../../../shared-data/head-function-export.js"

it(`Deduplicates multiple tags with same id`, () => {
cy.visit(headFunctionExportSharedData.page.deduplication).waitForRouteChange()

// deduplication link has id and should be deduplicated
cy.get(`link[rel=deduplication]`).should("have.length", 1)
// last deduplication link should win
cy.get(`link[rel=deduplication]`).should("have.attr", "href", "/bar")
// we should preserve id
cy.get(`link[rel=deduplication]`).should(
"have.attr",
"id",
"deduplication-test"
)

// alternate links are not using id, so should have multiple instances
cy.get(`link[rel=alternate]`).should("have.length", 2)
})
Expand Up @@ -11,6 +11,7 @@ const page = {
ssr: `${path}/ssr/`,
invalidElements: `${path}/invalid-elements/`,
fsRouteApi: `${path}/fs-route-api/`,
deduplication: `${path}/deduplication/`,
}

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

export default function HeadFunctionDeduplication() {
return (
<>
<h1>
I deduplicated Head elements by their <code>id</code>
</h1>
</>
)
}

function SEO({ children }) {
return (
<>
<link rel="deduplication" id="deduplication-test" href="/foo" />
<link
rel="alternate"
type="application/atom+xml"
title="RSS Feed"
href="/blog/news/atom"
/>
{children}
</>
)
}

export function Head() {
return (
<SEO>
<link rel="deduplication" id="deduplication-test" href="/bar" />
<link rel="alternate" hrefLang="de-DE" href="/de/" />
</SEO>
)
}
Expand Up @@ -76,4 +76,23 @@ describe(`Head function export SSR'ed HTML output`, () => {
expect(style.text).toContain(data.queried.style)
expect(link.attributes.href).toEqual(data.queried.link)
})

it(`deduplicates multiple tags with same id`, () => {
const html = readFileSync(`${publicDir}${page.deduplication}/index.html`)
const dom = parse(html)

// deduplication link has id and should be deduplicated
expect(dom.querySelectorAll(`link[rel=deduplication]`)?.length).toEqual(1)
// last deduplication link should win
expect(
dom.querySelector(`link[rel=deduplication]`)?.attributes?.href
).toEqual("/bar")
// we should preserve id
expect(
dom.querySelector(`link[rel=deduplication]`)?.attributes?.id
).toEqual("deduplication-test")

// alternate links are not using id, so should have multiple instances
expect(dom.querySelectorAll(`link[rel=alternate]`)?.length).toEqual(2)
})
})
1 change: 1 addition & 0 deletions integration-tests/head-function-export/package.json
Expand Up @@ -19,6 +19,7 @@
"babel-preset-gatsby-package": "^2.4.0",
"fs-extra": "^10.0.0",
"jest": "^27.2.1",
"node-html-parser": "^5.3.3",
"npm-run-all": "4.1.5"
},
"dependencies": {
Expand Down
Expand Up @@ -7,6 +7,7 @@ const page = {
staticQuery: `${path}/static-query-component/`,
warnings: `${path}/warnings/`,
allProps: `${path}/all-props/`,
deduplication: `${path}/deduplication/`,
}

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

export default function HeadFunctionDeduplication() {
return (
<>
<h1>
I deduplicated Head elements by their <code>id</code>
</h1>
</>
)
}

function SEO({ children }) {
return (
<>
<link rel="deduplication" id="deduplication-test" href="/foo" />
<link
rel="alternate"
type="application/atom+xml"
title="RSS Feed"
href="/blog/news/atom"
/>
{children}
</>
)
}

export function Head() {
return (
<SEO>
<link rel="deduplication" id="deduplication-test" href="/bar" />
<link rel="alternate" hrefLang="de-DE" href="/de/" />
</SEO>
)
}
Expand Up @@ -22,15 +22,28 @@ const onHeadRendered = () => {

removePrevHeadElements()

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 {
const clonedNode = node.cloneNode(true)
clonedNode.setAttribute(`data-gatsby-head`, true)
validHeadNodes.push(clonedNode)
if (id) {
if (!seenIds.has(id)) {
validHeadNodes.push(clonedNode)
seenIds.set(id, validHeadNodes.length - 1)
} else {
const indexOfPreviouslyInsertedNode = seenIds.get(id)
validHeadNodes[indexOfPreviouslyInsertedNode].remove()
validHeadNodes[indexOfPreviouslyInsertedNode] = clonedNode
}
} else {
validHeadNodes.push(clonedNode)
}
}
}

Expand Down
15 changes: 13 additions & 2 deletions packages/gatsby/cache-dir/head/head-export-handler-for-ssr.js
Expand Up @@ -54,8 +54,10 @@ export function headHandlerForSSR({

const validHeadNodes = []

const seenIds = new Map()
for (const node of headNodes) {
const { rawTagName, attributes } = node
const id = attributes.id

if (!VALID_NODE_NAMES.includes(rawTagName)) {
warnForInvalidTags(rawTagName)
Expand All @@ -68,8 +70,17 @@ export function headHandlerForSSR({
},
node.childNodes[0]?.textContent
)

validHeadNodes.push(element)
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)
}
}
}

Expand Down

0 comments on commit 6f0d46b

Please sign in to comment.