This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import serializeJavascript from 'serialize-javascript'; | |
import React from 'react'; | |
import { StaticRouter, matchPath } from 'react-router-dom'; | |
import { renderToStringWithData } from 'react-apollo'; | |
// [CS] ADDED FOR CODE SPLITTING | |
import Loadable from 'react-loadable'; | |
import Helmet from 'react-helmet'; | |
import GraphQLClientFactory from '../src/lib/GraphQLClientFactory'; | |
import config from '../src/temp/config'; | |
import i18ninit from '../src/i18n'; | |
import AppRoot, { routePatterns } from '../src/AppRoot'; | |
import { setServerSideRenderingState } from '../src/RouteHandler'; | |
import indexTemplate from '../build/index.html'; | |
// [CS] ADDED FOR CODE SPLITTING | |
import manifest from '../build/asset-manifest.json'; | |
/** | |
* [CS] Converts a CRA asset manifest into script tags, given a list of used modules | |
* produced by Loadable.Capture. | |
* | |
* This function is used instead of react-loadable's getBundles() function because | |
* CRA's webpack is not configurable to use the loadable plugin, | |
* so we consume CRA's slightly different format chunk manifest instead. | |
* | |
* We need these script tags to avoid a flash of "loading..." while the browser hydrates the | |
* code-split components. By telling the browser in advance which components we need, | |
* we can have their modules preloaded before the page renders. | |
*/ | |
function convertLoadableModulesToScripts(usedModules) { | |
return Object.keys(manifest) | |
.filter((chunkName) => usedModules.indexOf(chunkName.replace('.js', '')) > -1) | |
.map((k) => `<script src="${manifest[k]}"></script>`) | |
.join(''); | |
} | |
/** | |
* Main entry point to the application when run via Server-Side Rendering, | |
* either in Integrated Mode, or with a Node proxy host like the node-headless-ssr-proxy sample. | |
* This function will be invoked by the server to return the rendered HTML. | |
* @param {Function} callback Function to call when rendering is complete. Signature callback(error, successData). | |
* @param {string} path Current route path being rendered | |
* @param {string} data JSON Layout service data for the rendering from Sitecore | |
* @param {string} viewBag JSON view bag data from Sitecore (extensible context stuff) | |
*/ | |
export function renderView(callback, path, data, viewBag) { | |
try { | |
const state = parseServerData(data, viewBag); | |
setServerSideRenderingState(state); | |
/* | |
GraphQL Data | |
The Apollo Client needs to be initialized to make GraphQL available to the JSS app. | |
Not using GraphQL? Remove this, and the ApolloContext from `AppRoot`. | |
*/ | |
const graphQLClient = GraphQLClientFactory(config.graphQLEndpoint, true); | |
// [CS] Stores the list of loadable modules that were rendered during SSR | |
// we use this list to add <script> tags to import all of them to the SSR'ed HTML | |
const loadableModules = []; | |
/* | |
App Rendering | |
*/ | |
initializei18n(state) | |
// [CS] make react-loadable preload all dynamic imports - this will get us HTML during SSR | |
.then(() => Loadable.preloadAll()) | |
.then(() => | |
// renderToStringWithData() allows any GraphQL queries to complete their async call | |
// before the SSR result is returned, so that the resulting HTML from GQL query results | |
// is included in the SSR'ed markup instead of whatever the 'loading' state is. | |
// Not using GraphQL? Use ReactDOMServer.renderToString() instead. | |
renderToStringWithData( | |
// [CS] loadable.capture makes a list of code-split components that were used during SSR, | |
// so that we can pre-load them as client-side scripts to avoid any flashes of 'loading...' text. | |
<Loadable.Capture report={(module) => loadableModules.push(module)}> | |
<AppRoot path={path} Router={StaticRouter} graphQLClient={graphQLClient} /> | |
</Loadable.Capture> | |
) | |
) | |
.then((renderedAppHtml) => { | |
const helmet = Helmet.renderStatic(); | |
// We remove the viewBag from the server-side state before sending it back to the client. | |
// This saves bandwidth, because by default the viewBag contains the translation dictionary, | |
// which is better cached as a separate client HTTP request than on every page, and HTTP context | |
// information that is not meaningful to the client-side rendering. | |
// If you wish to place items in the viewbag that are needed by client-side rendering, this | |
// can be removed - but still delete state.viewBag.dictionary, at least. | |
delete state.viewBag; | |
// We add the GraphQL state to the SSR state so that we can avoid refetching queries after client load | |
// Not using GraphQL? Get rid of this. | |
state.APOLLO_STATE = graphQLClient.cache.extract(); | |
// Inject the rendered app into the index.html template (built from /public/index.html) | |
// IMPORTANT: use serialize-javascript or similar instead of JSON.stringify() to emit initial state, | |
// or else you're vulnerable to XSS. | |
const html = indexTemplate | |
// write the React app | |
.replace('<div id="root"></div>', `<div id="root">${renderedAppHtml}</div>`) | |
// write the string version of our state | |
.replace('__JSS_STATE__=null', `__JSS_STATE__=${serializeJavascript(state)}`) | |
// render <head> contents from react-helmet | |
.replace( | |
'<head>', | |
`<head>${helmet.title.toString()}${helmet.meta.toString()}${helmet.link.toString()}` | |
) | |
// [CS] We inject our used code-split modules' script tags before any other script tags. | |
// In this case, `<script>` is the opening tag to the __JSS_STATE__ script block in index.html. | |
.replace('<script>', `${convertLoadableModulesToScripts(loadableModules)}<script>`); | |
callback(null, { html }); | |
}) | |
.catch((error) => callback(error, null)); | |
} catch (err) { | |
// need to ensure the callback is always invoked no matter what | |
// or else SSR will hang | |
callback(err, null); | |
} | |
} | |
/** | |
* Parses an incoming url to match against the route table. This function is implicitly used | |
* by node-headless-ssr-proxy when rendering the site in headless mode. It enables rewriting the incoming path, | |
* say '/en-US/hello', to the path and language to pass to Layout Service (a Sitecore item path), say | |
* { sitecoreRoute: '/hello', lang: 'en-US' }. | |
* This function is _not_ used in integrated mode, as Sitecore's built in route parsing is used. | |
* If no URL transformations are required (i.e. single language site), then this function can be removed. | |
* @param {string} url The incoming URL to the proxy server | |
* @returns { sitecoreRoute?: string, lang?: string } | |
*/ | |
export function parseRouteUrl(url) { | |
if (!url) { | |
return null; | |
} | |
let result = null; | |
// use react-router-dom to find the route matching the incoming URL | |
// then return its match params | |
routePatterns.forEach((pattern) => { | |
const match = matchPath(url, { pattern }); | |
if (match && match.params) { | |
result = match.params; | |
} | |
}); | |
return result; | |
} | |
function parseServerData(data, viewBag) { | |
const parsedData = data instanceof Object ? data : JSON.parse(data); | |
const parsedViewBag = viewBag instanceof Object ? viewBag : JSON.parse(viewBag); | |
return { | |
viewBag: parsedViewBag, | |
sitecore: parsedData && parsedData.sitecore, | |
}; | |
} | |
function initializei18n(state) { | |
// don't init i18n for not found routes | |
if (!state || !state.sitecore || !state.sitecore.context) return Promise.resolve(); | |
return i18ninit(state.sitecore.context.language, state.viewBag.dictionary); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment