Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions packages/next-server/lib/head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
)
}
Expand Down Expand Up @@ -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 })
})
}

Expand Down
2 changes: 1 addition & 1 deletion packages/next-server/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
29 changes: 22 additions & 7 deletions packages/next/client/head-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,37 @@ 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
}
}
return true
})

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()
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/next/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 33 additions & 57 deletions test/integration/client-navigation/test/rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand All @@ -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,
Expand All @@ -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(
'<link rel="stylesheet" href="/dup-style.css" class="next-head"/><link rel="stylesheet" href="/dup-style.css" class="next-head"/>'
)
expect(html).toContain('<meta content="my meta"/>')
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"/>'
)
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"/>'
)
expect(html).toContain(
'<meta property="dedupe:tag" content="tag4" class="next-head"/>'
)
expect(html).toContain(
'<meta property="og:image" content="ogImageTag1" class="next-head"/>'
'<meta property="og:image" content="ogImageTag1"/>'
)
expect(html).toContain(
'<meta property="og:image" content="ogImageTag2" class="next-head"/>'
'<meta property="og:image" content="ogImageTag2"/>'
)
expect(html).toContain(
'<meta property="og:image:alt" content="ogImageAltTag1" class="next-head"/>'
'<meta property="og:image:alt" content="ogImageAltTag1"/>'
)
expect(html).toContain(
'<meta property="og:image:alt" content="ogImageAltTag2" class="next-head"/>'
'<meta property="og:image:alt" content="ogImageAltTag2"/>'
)
expect(html).toContain(
'<meta property="og:image:width" content="ogImageWidthTag1" class="next-head"/>'
'<meta property="og:image:width" content="ogImageWidthTag1"/>'
)
expect(html).toContain(
'<meta property="og:image:width" content="ogImageWidthTag2" class="next-head"/>'
'<meta property="og:image:width" content="ogImageWidthTag2"/>'
)
expect(html).toContain(
'<meta property="og:image:height" content="ogImageHeightTag1" class="next-head"/>'
'<meta property="og:image:height" content="ogImageHeightTag1"/>'
)
expect(html).toContain(
'<meta property="og:image:height" content="ogImageHeightTag2" class="next-head"/>'
'<meta property="og:image:height" content="ogImageHeightTag2"/>'
)
expect(html).toContain(
'<meta property="og:image:type" content="ogImageTypeTag1" class="next-head"/>'
'<meta property="og:image:type" content="ogImageTypeTag1"/>'
)
expect(html).toContain(
'<meta property="og:image:type" content="ogImageTypeTag2" class="next-head"/>'
'<meta property="og:image:type" content="ogImageTypeTag2"/>'
)
expect(html).toContain(
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag1" class="next-head"/>'
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag1"/>'
)
expect(html).toContain(
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag2" class="next-head"/>'
'<meta property="og:image:secure_url" content="ogImageSecureUrlTag2"/>'
)
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 () => {
Expand Down