import React from 'react' import withSideEffect from './side-effect' import { AmpModeContext } from './amphtml-context' import { HeadManagerContext } from './head-manager-context' import { isAmp } from './amp' type WithIsAmp = { isAmp?: boolean } export function defaultHead(className = 'next-head', isAmp = false) { const head = [] if (!isAmp) { head.push( ) } return head } function onlyReactElement( list: Array>, child: React.ReactChild ): Array> { // React children can be "string" or "number" in this case we ignore them for backwards compat if (typeof child === 'string' || typeof child === 'number') { return list } // Adds support for React.Fragment if (child.type === React.Fragment) { return list.concat( React.Children.toArray(child.props.children).reduce( ( fragmentList: Array>, fragmentChild: React.ReactChild ): Array> => { if ( typeof fragmentChild === 'string' || typeof fragmentChild === 'number' ) { return fragmentList } return fragmentList.concat(fragmentChild) }, [] ) ) } return list.concat(child) } const METATYPES = ['name', 'httpEquiv', 'charSet', 'itemProp'] /* returns a function for filtering head child elements which shouldn't be duplicated, like Also adds support for deduplicated `key` properties */ function unique() { const keys = new Set() const tags = new Set() const metaTypes = new Set() const metaCategories: { [metatype: string]: Set<string> } = {} return (h: React.ReactElement<any>) => { if (h.key && typeof h.key !== 'number' && h.key.indexOf('.$') === 0) { if (keys.has(h.key)) return false keys.add(h.key) return true } switch (h.type) { case 'title': case 'base': if (tags.has(h.type)) return false tags.add(h.type) break case 'meta': for (let i = 0, len = METATYPES.length; i < len; i++) { const metatype = METATYPES[i] if (!h.props.hasOwnProperty(metatype)) continue if (metatype === 'charSet') { if (metaTypes.has(metatype)) return false metaTypes.add(metatype) } else { const category = h.props[metatype] const categories = metaCategories[metatype] || new Set() if (categories.has(category)) return false categories.add(category) metaCategories[metatype] = categories } } break } return true } } /** * * @param headElement List of multiple <Head> instances */ function reduceComponents( headElements: Array<React.ReactElement<any>>, props: WithIsAmp ) { return headElements .reduce( (list: React.ReactChild[], headElement: React.ReactElement<any>) => { const headElementChildren = React.Children.toArray( headElement.props.children ) return list.concat(headElementChildren) }, [] ) .reduce(onlyReactElement, []) .reverse() .concat(defaultHead('', props.isAmp)) .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 }) }) } const Effect = withSideEffect() /** * 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. */ function Head({ children }: { children: React.ReactNode }) { return ( <AmpModeContext.Consumer> {ampMode => ( <HeadManagerContext.Consumer> {updateHead => ( <Effect reduceComponentsToState={reduceComponents} handleStateChange={updateHead} isAmp={isAmp(ampMode)} > {children} </Effect> )} </HeadManagerContext.Consumer> )} </AmpModeContext.Consumer> ) } Head.rewind = Effect.rewind export default Head