UNPKG

@plone/volto

Version:
214 lines (201 loc) 7.19 kB
/** * Html helper. * @module helpers/Html */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Helmet from '@plone/volto/helpers/Helmet/Helmet'; import serialize from 'serialize-javascript'; import join from 'lodash/join'; import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass'; import { runtimeConfig } from '@plone/volto/runtime_config'; import config from '@plone/volto/registry'; const CRITICAL_CSS_TEMPLATE = `function alter() { document.querySelectorAll("head link[rel='prefetch']").forEach(function(el) { el.rel = 'stylesheet'}); } if (window.addEventListener) { window.addEventListener('DOMContentLoaded', alter, false) } else { window.onload=alter }`; export const loadReducers = (state = {}) => { const { settings } = config; return Object.assign( {}, ...Object.keys(state).map((name) => settings.initialReducersBlacklist.includes(name) ? {} : { [name]: state[name] }, ), ); }; /** * Html class. * Wrapper component containing HTML metadata and boilerplate tags. * Used in server-side code only to wrap the string output of the * rendered route component. * * The only thing this component doesn't (and can't) include is the * HTML doctype declaration, which is added to the rendered output * by the server.js file. * * Critical.css behaviour: when a file `public/critical.css` is present, the * loading of stylesheets is changed. The styles in critical.css are inlined in * the generated HTML, and the whole story needs to change completely: instead * of treating stylesheets as priority for rendering, we want to defer their * loading as much as possible. So we change the stylesheets to be prefetched * and we switch their rel back to stylesheets at document ready event. * * @function Html * @param {Object} props Component properties. * @param {Object} props.assets Assets to be rendered. * @param {Object} props.component Content to be rendered as child node. * @param {Object} props.store Store object. * @returns {string} Markup of the not found page. */ /** * Html class. * @class Html * @extends Component */ class Html extends Component { /** * Property types. * @property {Object} propTypes Property types. * @static */ static propTypes = { extractor: PropTypes.shape({ getLinkElements: PropTypes.func.isRequired, getScriptElements: PropTypes.func.isRequired, getStyleElements: PropTypes.func.isRequired, }).isRequired, markup: PropTypes.string.isRequired, store: PropTypes.shape({ getState: PropTypes.func, }).isRequired, }; /** * Render method. * @method render * @returns {string} Markup for the component. */ render() { const { extractor, markup, store, criticalCss, apiPath, publicURL } = this.props; const head = Helmet.rewind(); const bodyClass = join(BodyClass.rewind(), ' '); const htmlAttributes = head.htmlAttributes.toComponent(); return ( <html lang={htmlAttributes.lang}> <head> <meta charSet="utf-8" /> {head.base.toComponent()} {head.title.toComponent()} {head.meta.toComponent()} {head.link.toComponent()} {head.script.toComponent()} {config.settings.cssLayers && ( // Load the CSS layers from config, if any <style>{`@layer ${config.settings.cssLayers.join(', ')};`}</style> )} {head.style.toComponent()} <script dangerouslySetInnerHTML={{ __html: `window.env = ${serialize({ ...runtimeConfig, // Seamless mode requirement, the client need to know where the API is located // if not set in the API_PATH ...(apiPath && { apiPath, }), ...(publicURL && { publicURL, }), ...(process.env.SITE_DEFAULT_LANGUAGE && { defaultLanguage: process.env.SITE_DEFAULT_LANGUAGE, }), })};`, }} /> <link rel="icon" href="/favicon.ico" sizes="any" /> <link rel="icon" href="/icon.svg" type="image/svg+xml" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="manifest" href="/site.webmanifest" /> <meta name="generator" content="Plone 6 - https://plone.org" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="mobile-web-app-capable" content="yes" /> {process.env.NODE_ENV === 'production' && criticalCss && ( <style dangerouslySetInnerHTML={{ __html: this.props.criticalCss }} /> )} {/* Add the crossorigin while in development */} {extractor.getLinkElements().map((elem) => React.cloneElement(elem, { crossOrigin: process.env.NODE_ENV === 'production' ? undefined : 'true', rel: !criticalCss ? elem.props.rel : elem.props.as === 'style' ? 'prefetch' : elem.props.rel, }), )} {/* Styles in development are loaded with Webpack's style-loader, in production, they need to be static*/} {process.env.NODE_ENV === 'production' ? ( criticalCss ? ( <> <script dangerouslySetInnerHTML={{ __html: CRITICAL_CSS_TEMPLATE, }} ></script> {extractor.getStyleElements().map((elem) => ( // eslint-disable-next-line react/jsx-key <noscript> {React.cloneElement(elem, { rel: 'stylesheet', crossOrigin: process.env.NODE_ENV === 'production' ? undefined : 'true', })} </noscript> ))} </> ) : ( extractor.getStyleElements() ) ) : undefined} </head> <body className={bodyClass}> <div role="navigation" aria-label="Toolbar" id="toolbar" /> <div id="main" dangerouslySetInnerHTML={{ __html: markup }} /> <div role="complementary" aria-label="Sidebar" id="sidebar" /> <script dangerouslySetInnerHTML={{ __html: `window.__data=${serialize( loadReducers(store.getState()), )};`, }} charSet="UTF-8" /> {/* Add the crossorigin while in development */} {extractor.getScriptElements().map((elem) => React.cloneElement(elem, { crossOrigin: process.env.NODE_ENV === 'production' ? undefined : 'true', }), )} </body> </html> ); } } export default Html;