@plone/volto
Version:
Volto
214 lines (201 loc) • 7.19 kB
JSX
/**
* 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;