ws-nextjs-app/pages/_document.page.tsx
/* eslint-disable react/no-danger */
import Document, {
DocumentContext,
Head,
Html,
Main,
NextScript,
} from 'next/document';
import Script from 'next/script';
import React, { HTMLAttributes, ReactElement } from 'react';
import { Helmet, HelmetData } from 'react-helmet';
import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import createCache from '@emotion/cache';
import isAppPath from '#app/routes/utils/isAppPath';
import isLitePath from '#app/routes/utils/isLitePath';
import {
EnvConfig,
getProcessEnvAppVariables,
} from '#lib/utilities/getEnvConfig';
import LiteRenderer from '#server/Document/Renderers/LiteRenderer';
import litePageTransforms from '#server/Document/Renderers/litePageTransforms';
import sendCustomMetric from '#server/utilities/customMetrics';
import { NON_200_RESPONSE } from '#server/utilities/customMetrics/metrics.const';
import nodeLogger from '#lib/logger.node';
import {
SERVER_SIDE_RENDER_REQUEST_RECEIVED,
SERVER_SIDE_REQUEST_FAILED,
} from '#lib/logger.const';
import { OK, INTERNAL_SERVER_ERROR } from '#app/lib/statusCodes.const';
import NO_JS_CLASSNAME from '#app/lib/noJs.const';
import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders';
import derivePageType from '../utilities/derivePageType';
const logger = nodeLogger(__filename);
const handleServerLogging = (ctx: DocumentContext) => {
const url = ctx.asPath || '';
const headers = removeSensitiveHeaders(ctx.req?.headers);
const pageType = derivePageType(url);
const { statusCode } = ctx.res || {};
const { cause, message, name, stack } = ctx.err || {};
switch (statusCode) {
case OK:
logger.debug(SERVER_SIDE_RENDER_REQUEST_RECEIVED, {
url,
headers,
pageType,
});
break;
case INTERNAL_SERVER_ERROR:
sendCustomMetric({
metricName: NON_200_RESPONSE,
statusCode,
pageType,
requestUrl: url,
});
logger.error(SERVER_SIDE_REQUEST_FAILED, {
status: INTERNAL_SERVER_ERROR,
message: { cause, message, name, stack, url },
url,
headers,
pageType,
});
break;
default:
break;
}
};
type DocProps = {
clientSideEnvVariables: EnvConfig;
css: string;
helmet: HelmetData;
htmlAttrs: HTMLAttributes<HTMLHtmlElement>;
ids: string[];
isApp: boolean;
isLite: boolean;
title: ReactElement;
};
export default class AppDocument extends Document<DocProps> {
static async getInitialProps(ctx: DocumentContext) {
const url = ctx.asPath || '';
const isApp = isAppPath(url);
const isLite = isLitePath(url);
const cache = createCache({ key: 'css' });
const { extractCritical } = createEmotionServer(cache);
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => (
<CacheProvider value={cache}>
<App {...props} />
</CacheProvider>
),
});
const initialProps = await Document.getInitialProps(ctx);
if (isLite) {
initialProps.html = litePageTransforms(initialProps.html);
}
const { css, ids } = extractCritical(initialProps.html);
// Read env variables from the server and expose them to the client
const clientSideEnvVariables = getProcessEnvAppVariables();
handleServerLogging(ctx);
return {
...initialProps,
clientSideEnvVariables,
css,
helmet: Helmet.renderStatic(),
ids,
isApp,
isLite,
};
}
render() {
const { clientSideEnvVariables, css, helmet, ids, isApp, isLite } =
this.props;
const htmlAttrs = helmet.htmlAttributes.toComponent();
const title = helmet.title.toComponent();
const helmetMetaTags = helmet.meta.toComponent();
const helmetLinkTags = helmet.link.toComponent();
const helmetScriptTags = helmet.script.toComponent();
switch (true) {
case isLite:
return (
<LiteRenderer
bodyContent={<Main />}
helmetLinkTags={helmetLinkTags}
helmetMetaTags={helmetMetaTags}
helmetScriptTags={helmetScriptTags}
htmlAttrs={htmlAttrs}
styles={css}
title={title}
/>
);
default:
return (
<Html lang="en-GB" {...htmlAttrs} className={NO_JS_CLASSNAME}>
<Head>
<script
type="text/javascript"
dangerouslySetInnerHTML={{
__html: `document.documentElement.classList.remove("no-js");`,
}}
/>
<Script strategy="beforeInteractive">
{`window.SIMORGH_ENV_VARS=${JSON.stringify(clientSideEnvVariables)}`}
</Script>
{isApp && <meta name="robots" content="noindex" />}
{title}
{helmetMetaTags}
{helmetLinkTags}
{helmetScriptTags}
<style
data-emotion={ids.join(' ')}
dangerouslySetInnerHTML={{ __html: css }}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
}