Add next-head-count & remove next-head class (#8020)
Fixes #3494 Removes `class="next-head"` from the children of the `<Head>` component. Instead, a single sentinel meta element named `next-head-count` is appended. The content is the number of contiguous elements immediately preceding the sentinel that _would have had_ the `class="next-head"` attribute. During an update, instead of searching for `class="next-head"`, the sentinel is located and the N previous elements are considered candidates for `oldTags`. New elements are inserted before the sentinel, and finally the sentinel is updated to reflect the new count.
This commit is contained in:
parent
3ee5ec4412
commit
e68307df3a
5 changed files with 64 additions and 77 deletions
|
@ -8,15 +8,14 @@ type WithInAmpMode = {
|
|||
inAmpMode?: boolean
|
||||
}
|
||||
|
||||
export function defaultHead(className = 'next-head', inAmpMode = false) {
|
||||
const head = [<meta key="charSet" charSet="utf-8" className={className} />]
|
||||
export function defaultHead(inAmpMode = false) {
|
||||
const head = [<meta key="charSet" charSet="utf-8" />]
|
||||
if (!inAmpMode) {
|
||||
head.push(
|
||||
<meta
|
||||
key="viewport"
|
||||
name="viewport"
|
||||
content="width=device-width,minimum-scale=1,initial-scale=1"
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -121,19 +120,12 @@ function reduceComponents(
|
|||
)
|
||||
.reduce(onlyReactElement, [])
|
||||
.reverse()
|
||||
.concat(defaultHead('', props.inAmpMode))
|
||||
.concat(defaultHead(props.inAmpMode))
|
||||
.filter(unique())
|
||||
.reverse()
|
||||
.map((c: React.ReactElement<any>, i: number) => {
|
||||
let className: string | undefined =
|
||||
(c.props && c.props.className ? c.props.className + ' ' : '') +
|
||||
'next-head'
|
||||
|
||||
if (c.type === 'title' && !c.props.className) {
|
||||
className = undefined
|
||||
}
|
||||
const key = c.key || i
|
||||
return React.cloneElement(c, { key, className })
|
||||
return React.cloneElement(c, { key })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ function render(
|
|||
try {
|
||||
html = renderElementToString(element)
|
||||
} finally {
|
||||
head = Head.rewind() || defaultHead(undefined, isInAmpMode(ampMode))
|
||||
head = Head.rewind() || defaultHead(isInAmpMode(ampMode))
|
||||
}
|
||||
|
||||
return { html, head }
|
||||
|
|
|
@ -46,14 +46,24 @@ export default class HeadManager {
|
|||
|
||||
updateElements (type, components) {
|
||||
const headEl = document.getElementsByTagName('head')[0]
|
||||
const oldTags = Array.prototype.slice.call(
|
||||
headEl.querySelectorAll(type + '.next-head')
|
||||
)
|
||||
const headCountEl = headEl.querySelector('meta[name=next-head-count]')
|
||||
const headCount = Number(headCountEl.content)
|
||||
const oldTags = []
|
||||
|
||||
for (
|
||||
let i = 0, j = headCountEl.previousElementSibling;
|
||||
i < headCount;
|
||||
i++, j = j.previousElementSibling
|
||||
) {
|
||||
if (j.tagName.toLowerCase() === type) {
|
||||
oldTags.push(j)
|
||||
}
|
||||
}
|
||||
const newTags = components.map(reactElementToDOM).filter(newTag => {
|
||||
for (let i = 0, len = oldTags.length; i < len; i++) {
|
||||
const oldTag = oldTags[i]
|
||||
for (let k = 0, len = oldTags.length; k < len; k++) {
|
||||
const oldTag = oldTags[k]
|
||||
if (oldTag.isEqualNode(newTag)) {
|
||||
oldTags.splice(i, 1)
|
||||
oldTags.splice(k, 1)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +71,12 @@ export default class HeadManager {
|
|||
})
|
||||
|
||||
oldTags.forEach(t => t.parentNode.removeChild(t))
|
||||
newTags.forEach(t => headEl.appendChild(t))
|
||||
newTags.forEach(t => headEl.insertBefore(t, headCountEl))
|
||||
headCountEl.content = (
|
||||
headCount -
|
||||
oldTags.length +
|
||||
newTags.length
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -331,6 +331,10 @@ export class Head extends Component<
|
|||
<head {...this.props}>
|
||||
{children}
|
||||
{head}
|
||||
<meta
|
||||
name="next-head-count"
|
||||
content={React.Children.count(head || []).toString()}
|
||||
/>
|
||||
{inAmpMode && (
|
||||
<>
|
||||
<meta
|
||||
|
|
|
@ -13,9 +13,7 @@ export default function (render, fetch) {
|
|||
describe('Rendering via HTTP', () => {
|
||||
test('renders a stateless component', async () => {
|
||||
const html = await render('/stateless')
|
||||
expect(
|
||||
html.includes('<meta charSet="utf-8" class="next-head"/>')
|
||||
).toBeTruthy()
|
||||
expect(html.includes('<meta charSet="utf-8"/>')).toBeTruthy()
|
||||
expect(html.includes('My component!')).toBeTruthy()
|
||||
})
|
||||
|
||||
|
@ -39,39 +37,33 @@ export default function (render, fetch) {
|
|||
// default-head contains an empty <Head />.
|
||||
test('header renders default charset', async () => {
|
||||
const html = await render('/default-head')
|
||||
expect(
|
||||
html.includes('<meta charSet="utf-8" class="next-head"/>')
|
||||
).toBeTruthy()
|
||||
expect(html.includes('<meta charSet="utf-8"/>')).toBeTruthy()
|
||||
expect(html.includes('next-head, but only once.')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('header renders default viewport', async () => {
|
||||
const html = await render('/default-head')
|
||||
expect(html).toContain(
|
||||
'<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1" class="next-head"/>'
|
||||
'<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"/>'
|
||||
)
|
||||
})
|
||||
|
||||
test('header helper renders header information', async () => {
|
||||
const html = await render('/head')
|
||||
expect(
|
||||
html.includes('<meta charSet="iso-8859-5" class="next-head"/>')
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
html.includes('<meta content="my meta" class="next-head"/>')
|
||||
).toBeTruthy()
|
||||
expect(html.includes('<meta charSet="iso-8859-5"/>')).toBeTruthy()
|
||||
expect(html.includes('<meta content="my meta"/>')).toBeTruthy()
|
||||
expect(html).toContain(
|
||||
'<meta name="viewport" content="width=device-width,initial-scale=1" class="next-head"/>'
|
||||
'<meta name="viewport" content="width=device-width,initial-scale=1"/>'
|
||||
)
|
||||
expect(html.includes('I can have meta tags')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('header helper dedupes tags', async () => {
|
||||
const html = await render('/head')
|
||||
expect(html).toContain('<meta charSet="iso-8859-5" class="next-head"/>')
|
||||
expect(html).not.toContain('<meta charSet="utf-8" class="next-head"/>')
|
||||
expect(html).toContain('<meta charSet="iso-8859-5"/>')
|
||||
expect(html).not.toContain('<meta charSet="utf-8"/>')
|
||||
expect(html).toContain(
|
||||
'<meta name="viewport" content="width=device-width,initial-scale=1" class="next-head"/>'
|
||||
'<meta name="viewport" content="width=device-width,initial-scale=1"/>'
|
||||
)
|
||||
expect(html.match(/<meta name="viewport" /g).length).toBe(
|
||||
1,
|
||||
|
@ -80,85 +72,69 @@ export default function (render, fetch) {
|
|||
expect(html).not.toContain(
|
||||
'<meta name="viewport" content="width=device-width"/>'
|
||||
)
|
||||
expect(html).toContain('<meta content="my meta" class="next-head"/>')
|
||||
expect(html).toContain('<meta content="my meta"/>')
|
||||
expect(html).toContain(
|
||||
'<link rel="stylesheet" href="/dup-style.css" class="next-head"/><link rel="stylesheet" href="/dup-style.css" class="next-head"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<link rel="stylesheet" href="dedupe-style.css" class="next-head"/>'
|
||||
'<link rel="stylesheet" href="/dup-style.css"/><link rel="stylesheet" href="/dup-style.css"/>'
|
||||
)
|
||||
expect(html).toContain('<link rel="stylesheet" href="dedupe-style.css"/>')
|
||||
expect(html).not.toContain(
|
||||
'<link rel="stylesheet" href="dedupe-style.css" class="next-head"/><link rel="stylesheet" href="dedupe-style.css" class="next-head"/>'
|
||||
'<link rel="stylesheet" href="dedupe-style.css"/><link rel="stylesheet" href="dedupe-style.css"/>'
|
||||
)
|
||||
})
|
||||
|
||||
test('header helper avoids dedupe of specific tags', async () => {
|
||||
const html = await render('/head')
|
||||
expect(html).toContain('<meta property="article:tag" content="tag1"/>')
|
||||
expect(html).toContain('<meta property="article:tag" content="tag2"/>')
|
||||
expect(html).not.toContain('<meta property="dedupe:tag" content="tag3"/>')
|
||||
expect(html).toContain('<meta property="dedupe:tag" content="tag4"/>')
|
||||
expect(html).toContain(
|
||||
'<meta property="article:tag" content="tag1" class="next-head"/>'
|
||||
'<meta property="og:image" content="ogImageTag1"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="article:tag" content="tag2" class="next-head"/>'
|
||||
)
|
||||
expect(html).not.toContain(
|
||||
'<meta property="dedupe:tag" content="tag3" class="next-head"/>'
|
||||
'<meta property="og:image" content="ogImageTag2"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="dedupe:tag" content="tag4" class="next-head"/>'
|
||||
'<meta property="og:image:alt" content="ogImageAltTag1"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image" content="ogImageTag1" class="next-head"/>'
|
||||
'<meta property="og:image:alt" content="ogImageAltTag2"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image" content="ogImageTag2" class="next-head"/>'
|
||||
'<meta property="og:image:width" content="ogImageWidthTag1"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:alt" content="ogImageAltTag1" class="next-head"/>'
|
||||
'<meta property="og:image:width" content="ogImageWidthTag2"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:alt" content="ogImageAltTag2" class="next-head"/>'
|
||||
'<meta property="og:image:height" content="ogImageHeightTag1"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:width" content="ogImageWidthTag1" class="next-head"/>'
|
||||
'<meta property="og:image:height" content="ogImageHeightTag2"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:width" content="ogImageWidthTag2" class="next-head"/>'
|
||||
'<meta property="og:image:type" content="ogImageTypeTag1"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:height" content="ogImageHeightTag1" class="next-head"/>'
|
||||
'<meta property="og:image:type" content="ogImageTypeTag2"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:height" content="ogImageHeightTag2" class="next-head"/>'
|
||||
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag1"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:type" content="ogImageTypeTag1" class="next-head"/>'
|
||||
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag2"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:type" content="ogImageTypeTag2" class="next-head"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag1" class="next-head"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag2" class="next-head"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="og:image:url" content="ogImageUrlTag1" class="next-head"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="fb:pages" content="fbpages1" class="next-head"/>'
|
||||
)
|
||||
expect(html).toContain(
|
||||
'<meta property="fb:pages" content="fbpages2" class="next-head"/>'
|
||||
'<meta property="og:image:url" content="ogImageUrlTag1"/>'
|
||||
)
|
||||
expect(html).toContain('<meta property="fb:pages" content="fbpages1"/>')
|
||||
expect(html).toContain('<meta property="fb:pages" content="fbpages2"/>')
|
||||
})
|
||||
|
||||
test('header helper renders Fragment children', async () => {
|
||||
const html = await render('/head')
|
||||
expect(html).toContain('<title>Fragment title</title>')
|
||||
expect(html).toContain(
|
||||
'<meta content="meta fragment" class="next-head"/>'
|
||||
)
|
||||
expect(html).toContain('<meta content="meta fragment"/>')
|
||||
})
|
||||
|
||||
it('should render the page with custom extension', async () => {
|
||||
|
|
Loading…
Reference in a new issue