UNPKG

@datawheel/canon-core

Version:

Reusable React environment and components for creating visualization engines.

196 lines (168 loc) 8.09 kB
/* global __TIMESTAMP__ */ import React from "react"; import Helmet from "react-helmet"; import {renderToString} from "react-dom/server"; import {createMemoryHistory, match, RouterContext} from "react-router"; import {I18nextProvider} from "react-i18next"; import {Provider} from "react-redux"; import createRoutes from "routes"; import configureStore from "./storeConfig"; import preRenderMiddleware from "./middlewares/preRenderMiddleware"; import pretty from "pretty"; import CanonProvider from "./CanonProvider"; import serialize from "serialize-javascript"; const tagManagerHead = process.env.CANON_GOOGLE_TAG_MANAGER === undefined ? "" : ` <!-- Google Tag Manager --> <script> (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','${process.env.CANON_GOOGLE_TAG_MANAGER}'); </script> <!-- End Google Tag Manager --> `; const tagManagerBody = process.env.CANON_GOOGLE_TAG_MANAGER === undefined ? "" : ` <!-- Google Tag Manager (noscript) --> <noscript> <iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.CANON_GOOGLE_TAG_MANAGER}" height="0" width="0" style="display:none;visibility:hidden"></iframe> </noscript> <!-- End Google Tag Manager (noscript) --> `; const analtyicsScript = process.env.CANON_GOOGLE_ANALYTICS === undefined ? "" : ` <!-- Google Analytics --> <script> (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); ${process.env.CANON_GOOGLE_ANALYTICS.split(",").map((key, i) => `ga('create', '${key}', 'auto', 'tracker${i + 1}');`).join("\n ")} ${process.env.CANON_GOOGLE_ANALYTICS.split(",").map((key, i) => `ga('tracker${i + 1}.send', 'pageview');`).join("\n ")} </script> <!-- End Google Analytics --> `; const pixelScript = process.env.CANON_FACEBOOK_PIXEL === undefined ? "" : ` <!-- Facebook Pixel --> <script> !function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){ n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)}; if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window,document,'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '${process.env.CANON_FACEBOOK_PIXEL}'); fbq('track', 'PageView'); </script> <!-- End Facebook Pixel --> `; const hotjarScript = process.env.CANON_HOTJAR === undefined ? "" : ` <!-- Hotjar --> <script> (function(h,o,t,j,a,r){ h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)}; h._hjSettings={hjid:${process.env.CANON_HOTJAR},hjsv:6}; a=o.getElementsByTagName('head')[0]; r=o.createElement('script');r.async=1; r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv; a.appendChild(r); })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv='); </script> <!-- End Hotjar --> `; const BASE_URL = process.env.CANON_BASE_URL || "/"; const basename = BASE_URL.replace(/^[A-z]{4,5}\:\/{2}[A-z0-9\.\-]{1,}\:{0,}[0-9]{0,4}/g, ""); const baseTag = process.env.CANON_BASE_URL === undefined ? "" : ` <base href='${ BASE_URL }'>`; /** Returns the default server logic for rendering a page. */ export default function(defaultStore = {}, headerConfig, reduxMiddleware = false) { return function(req, res) { const locale = req.i18n.language, resources = req.i18n.getResourceBundle(req.i18n.language); const windowLocation = { basename, host: req.headers.host, hostname: req.headers.host.split(":")[0], href: `${ req.protocol }://${ req.headers.host }${ req.url }`, origin: `${ req.protocol }://${ req.headers.host }`, pathname: req.url.split("?")[0], port: req.headers.host.includes(":") ? req.headers.host.split(":")[1] : "80", protocol: `${ req.protocol }:`, query: req.query, search: req.url.includes("?") ? `?${req.url.split("?")[1]}` : "" }; const location = req.url.replace(BASE_URL, ""); const history = createMemoryHistory({basename, entries: [location]}); const store = configureStore({i18n: {locale, resources}, location: windowLocation, ...defaultStore}, history, reduxMiddleware); const routes = createRoutes(store); const rtl = ["ar", "he"].includes(locale); match({history, routes}, (err, redirect, props) => { if (err) res.status(500).json(err); else if (redirect) res.redirect(302, `${redirect.basename}${redirect.pathname}${redirect.hash}${redirect.search}`); else if (props) { // This method waits for all render component // promises to resolve before returning to browser preRenderMiddleware(store, props) .then(() => { const initialState = store.getState(); const componentHTML = renderToString( <I18nextProvider i18n={req.i18n}> <Provider store={store}> <CanonProvider helmet={headerConfig} locale={locale}> <RouterContext {...props} /> </CanonProvider> </Provider> </I18nextProvider> ); const header = Helmet.rewind(); const htmlAttrs = header.htmlAttributes.toString().replace(" amp", ""); const defaultAttrs = headerConfig.htmlAttributes ? Object.keys(headerConfig.htmlAttributes) .map(key => { const val = headerConfig.htmlAttributes[key]; return ` ${key}${val ? `="${val}"` : ""}`; }) .join("") : ""; let status = 200; for (const key in initialState.data) { if ({}.hasOwnProperty.call(initialState.data, key)) { const error = initialState.data[key] ? initialState.data[key].error : null; if (error && typeof error === "number" && error > status) status = error; } } res.status(status).send(`<!doctype html> <html dir="${ rtl ? "rtl" : "ltr" }" ${htmlAttrs}${defaultAttrs}> <head> ${tagManagerHead}${pixelScript}${baseTag} ${ pretty(header.title.toString()).replace(/\n/g, "\n ") } ${ pretty(header.meta.toString()).replace(/\n/g, "\n ") } ${ pretty(header.link.toString()).replace(/\n/g, "\n ") } <link rel='stylesheet' type='text/css' href='${ process.env.CANON_BASE_URL ? "" : "/" }assets/normalize.css'> <link rel='stylesheet' type='text/css' href='${ process.env.CANON_BASE_URL ? "" : "/" }assets/styles.css?v${__TIMESTAMP__}'> ${hotjarScript} </head> <body> ${tagManagerBody} <div id="React-Container">${ componentHTML }</div> <script> window.__SSR__ = true; window.__APP_NAME__ = "${ req.i18n.options.defaultNS }"; window.__HELMET_DEFAULT__ = ${ serialize(headerConfig, {isJSON: true, space: 2}).replace(/\n/g, "\n ") }; window.__INITIAL_STATE__ = ${ serialize(initialState, {isJSON: true, space: 2}).replace(/\n/g, "\n ") }; </script> ${analtyicsScript} <script type="text/javascript" charset="utf-8" src="${ process.env.CANON_BASE_URL ? "" : "/" }assets/app.js?v${__TIMESTAMP__}"></script> </body> </html>`); }) .catch(err => { res.status(500).send({error: err.toString(), stackTrace: err.stack.toString()}); }); } else res.sendStatus(404); }); }; }