2020-06-12 00:09:06 +02:00
|
|
|
import React, { useContext } from 'react'
|
|
|
|
import Effect from './side-effect'
|
2019-06-27 16:22:24 +02:00
|
|
|
import { AmpStateContext } from './amp-context'
|
2019-05-30 03:19:32 +02:00
|
|
|
import { HeadManagerContext } from './head-manager-context'
|
2019-06-27 16:22:24 +02:00
|
|
|
import { isInAmpMode } from './amp'
|
2019-01-25 16:43:12 +01:00
|
|
|
|
2019-06-27 16:22:24 +02:00
|
|
|
type WithInAmpMode = {
|
|
|
|
inAmpMode?: boolean
|
2019-03-27 17:46:44 +01:00
|
|
|
}
|
|
|
|
|
2020-05-25 00:44:05 +02:00
|
|
|
export function defaultHead(inAmpMode = false): JSX.Element[] {
|
2019-11-26 18:27:33 +01:00
|
|
|
const head = [<meta charSet="utf-8" />]
|
2019-06-27 16:22:24 +02:00
|
|
|
if (!inAmpMode) {
|
2020-03-05 11:03:15 +01:00
|
|
|
head.push(<meta name="viewport" content="width=device-width" />)
|
2019-03-27 17:46:44 +01:00
|
|
|
}
|
2019-05-30 03:19:32 +02:00
|
|
|
return head
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
function onlyReactElement(
|
|
|
|
list: Array<React.ReactElement<any>>,
|
2019-05-30 03:19:32 +02:00
|
|
|
child: React.ReactChild
|
2019-01-25 16:43:12 +01:00
|
|
|
): Array<React.ReactElement<any>> {
|
|
|
|
// React children can be "string" or "number" in this case we ignore them for backwards compat
|
2019-05-30 03:19:32 +02:00
|
|
|
if (typeof child === 'string' || typeof child === 'number') {
|
|
|
|
return list
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
|
|
|
// Adds support for React.Fragment
|
|
|
|
if (child.type === React.Fragment) {
|
|
|
|
return list.concat(
|
2019-05-30 03:19:32 +02:00
|
|
|
React.Children.toArray(child.props.children).reduce(
|
|
|
|
(
|
|
|
|
fragmentList: Array<React.ReactElement<any>>,
|
|
|
|
fragmentChild: React.ReactChild
|
|
|
|
): Array<React.ReactElement<any>> => {
|
|
|
|
if (
|
|
|
|
typeof fragmentChild === 'string' ||
|
|
|
|
typeof fragmentChild === 'number'
|
|
|
|
) {
|
|
|
|
return fragmentList
|
|
|
|
}
|
|
|
|
return fragmentList.concat(fragmentChild)
|
|
|
|
},
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
)
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
2019-05-30 03:19:32 +02:00
|
|
|
return list.concat(child)
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
|
|
|
|
2019-06-10 18:41:43 +02:00
|
|
|
const METATYPES = ['name', 'httpEquiv', 'charSet', 'itemProp']
|
2019-01-25 16:43:12 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
returns a function for filtering head child elements
|
|
|
|
which shouldn't be duplicated, like <title/>
|
|
|
|
Also adds support for deduplicated `key` properties
|
|
|
|
*/
|
|
|
|
function unique() {
|
2019-05-30 03:19:32 +02:00
|
|
|
const keys = new Set()
|
|
|
|
const tags = new Set()
|
|
|
|
const metaTypes = new Set()
|
|
|
|
const metaCategories: { [metatype: string]: Set<string> } = {}
|
2019-01-25 16:43:12 +01:00
|
|
|
|
|
|
|
return (h: React.ReactElement<any>) => {
|
2020-06-01 23:00:22 +02:00
|
|
|
let isUnique = true
|
2021-01-03 11:35:51 +01:00
|
|
|
let hasKey = false
|
2019-10-18 07:40:54 +02:00
|
|
|
|
2019-11-26 18:27:33 +01:00
|
|
|
if (h.key && typeof h.key !== 'number' && h.key.indexOf('$') > 0) {
|
2021-01-03 11:35:51 +01:00
|
|
|
hasKey = true
|
2019-11-26 18:27:33 +01:00
|
|
|
const key = h.key.slice(h.key.indexOf('$') + 1)
|
|
|
|
if (keys.has(key)) {
|
2020-06-01 23:00:22 +02:00
|
|
|
isUnique = false
|
2019-11-26 18:27:33 +01:00
|
|
|
} else {
|
|
|
|
keys.add(key)
|
|
|
|
}
|
|
|
|
}
|
2019-10-18 07:40:54 +02:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
// eslint-disable-next-line default-case
|
2019-01-25 16:43:12 +01:00
|
|
|
switch (h.type) {
|
2019-05-30 03:19:32 +02:00
|
|
|
case 'title':
|
|
|
|
case 'base':
|
2019-11-26 18:27:33 +01:00
|
|
|
if (tags.has(h.type)) {
|
2020-06-01 23:00:22 +02:00
|
|
|
isUnique = false
|
2019-11-26 18:27:33 +01:00
|
|
|
} else {
|
|
|
|
tags.add(h.type)
|
|
|
|
}
|
2019-05-30 03:19:32 +02:00
|
|
|
break
|
|
|
|
case 'meta':
|
2019-01-25 16:43:12 +01:00
|
|
|
for (let i = 0, len = METATYPES.length; i < len; i++) {
|
2019-05-30 03:19:32 +02:00
|
|
|
const metatype = METATYPES[i]
|
|
|
|
if (!h.props.hasOwnProperty(metatype)) continue
|
2019-01-25 16:43:12 +01:00
|
|
|
|
2019-06-10 18:41:43 +02:00
|
|
|
if (metatype === 'charSet') {
|
2019-11-26 18:27:33 +01:00
|
|
|
if (metaTypes.has(metatype)) {
|
2020-06-01 23:00:22 +02:00
|
|
|
isUnique = false
|
2019-11-26 18:27:33 +01:00
|
|
|
} else {
|
|
|
|
metaTypes.add(metatype)
|
|
|
|
}
|
2019-01-25 16:43:12 +01:00
|
|
|
} else {
|
2019-05-30 03:19:32 +02:00
|
|
|
const category = h.props[metatype]
|
|
|
|
const categories = metaCategories[metatype] || new Set()
|
2021-01-03 11:35:51 +01:00
|
|
|
if ((metatype !== 'name' || !hasKey) && categories.has(category)) {
|
2020-06-01 23:00:22 +02:00
|
|
|
isUnique = false
|
2019-11-26 18:27:33 +01:00
|
|
|
} else {
|
|
|
|
categories.add(category)
|
|
|
|
metaCategories[metatype] = categories
|
|
|
|
}
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
|
|
|
}
|
2019-05-30 03:19:32 +02:00
|
|
|
break
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
2019-11-26 18:27:33 +01:00
|
|
|
|
2020-06-01 23:00:22 +02:00
|
|
|
return isUnique
|
2019-05-30 03:19:32 +02:00
|
|
|
}
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
2020-03-25 13:26:53 +01:00
|
|
|
* @param headElements List of multiple <Head> instances
|
2019-01-25 16:43:12 +01:00
|
|
|
*/
|
2019-05-30 03:19:32 +02:00
|
|
|
function reduceComponents(
|
|
|
|
headElements: Array<React.ReactElement<any>>,
|
2019-06-27 16:22:24 +02:00
|
|
|
props: WithInAmpMode
|
2019-05-30 03:19:32 +02:00
|
|
|
) {
|
2019-01-25 16:43:12 +01:00
|
|
|
return headElements
|
|
|
|
.reduce(
|
|
|
|
(list: React.ReactChild[], headElement: React.ReactElement<any>) => {
|
|
|
|
const headElementChildren = React.Children.toArray(
|
2019-05-30 03:19:32 +02:00
|
|
|
headElement.props.children
|
|
|
|
)
|
|
|
|
return list.concat(headElementChildren)
|
2019-01-25 16:43:12 +01:00
|
|
|
},
|
2019-05-30 03:19:32 +02:00
|
|
|
[]
|
2019-01-25 16:43:12 +01:00
|
|
|
)
|
|
|
|
.reduce(onlyReactElement, [])
|
|
|
|
.reverse()
|
2019-07-25 18:39:09 +02:00
|
|
|
.concat(defaultHead(props.inAmpMode))
|
2019-01-25 16:43:12 +01:00
|
|
|
.filter(unique())
|
|
|
|
.reverse()
|
|
|
|
.map((c: React.ReactElement<any>, i: number) => {
|
2019-05-30 03:19:32 +02:00
|
|
|
const key = c.key || i
|
2020-12-21 20:26:00 +01:00
|
|
|
if (
|
|
|
|
process.env.NODE_ENV !== 'development' &&
|
|
|
|
process.env.__NEXT_OPTIMIZE_FONTS &&
|
|
|
|
!props.inAmpMode
|
|
|
|
) {
|
2020-07-28 12:19:28 +02:00
|
|
|
if (
|
|
|
|
c.type === 'link' &&
|
|
|
|
c.props['href'] &&
|
|
|
|
// TODO(prateekbh@): Replace this with const from `constants` when the tree shaking works.
|
|
|
|
['https://fonts.googleapis.com/css'].some((url) =>
|
|
|
|
c.props['href'].startsWith(url)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
const newProps = { ...(c.props || {}) }
|
|
|
|
newProps['data-href'] = newProps['href']
|
|
|
|
newProps['href'] = undefined
|
|
|
|
return React.cloneElement(c, newProps)
|
|
|
|
}
|
|
|
|
}
|
2019-07-25 18:39:09 +02:00
|
|
|
return React.cloneElement(c, { key })
|
2019-05-30 03:19:32 +02:00
|
|
|
})
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
|
|
|
|
2019-05-23 21:31:22 +02:00
|
|
|
/**
|
|
|
|
* This component injects elements to `<head>` of your page.
|
|
|
|
* To avoid duplicated `tags` in `<head>` you can use the `key` property, which will make sure every tag is only rendered once.
|
|
|
|
*/
|
2019-04-24 16:47:50 +02:00
|
|
|
function Head({ children }: { children: React.ReactNode }) {
|
2020-06-12 00:09:06 +02:00
|
|
|
const ampState = useContext(AmpStateContext)
|
|
|
|
const headManager = useContext(HeadManagerContext)
|
2019-01-25 16:43:12 +01:00
|
|
|
return (
|
2020-06-12 00:09:06 +02:00
|
|
|
<Effect
|
|
|
|
reduceComponentsToState={reduceComponents}
|
|
|
|
headManager={headManager}
|
|
|
|
inAmpMode={isInAmpMode(ampState)}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</Effect>
|
2019-05-30 03:19:32 +02:00
|
|
|
)
|
2019-01-25 16:43:12 +01:00
|
|
|
}
|
|
|
|
|
2020-06-12 00:09:06 +02:00
|
|
|
// TODO: Remove in the next major release
|
|
|
|
Head.rewind = () => {}
|
2019-04-24 16:47:50 +02:00
|
|
|
|
|
|
|
export default Head
|